HMAC Signing

Every API request is authenticated by HMAC-SHA256 request signing. There are no long-lived bearer tokens; each request carries a fresh signature so it cannot be replayed.

Signing headers

HeaderDescription
x-api-keyYour access key id (public).
x-timestampUnix time in seconds when the request was created.
x-nonceA random string, unique per request (e.g. 16 random bytes in hex).
x-body-hashhex(sha256(rawBody)) — the hash of the exact request body.
x-signaturehex(HMAC-SHA256(secret, canonicalString)).

Canonical string

The signature is computed over this exact string, joined with \n:

TEA1-HMAC-SHA256
<METHOD>
<path>
<canonicalQuery>
<timestamp>
<nonce>
<bodyHash>
  • <METHOD> is uppercased (GET / POST).
  • <path> is the full path including the /api/v1 prefix — e.g. /api/v1/energy/quotes.
  • <canonicalQuery> is the query string with parameters sorted and re-encoded; empty when there is no query string.
  • <timestamp>, <nonce> and <bodyHash> come from the headers above.

Two things that trip people up

  1. The path includes /api/v1. Sign /api/v1/energy/quotes, not /energy/quotes.
  2. x-body-hash is never omitted. For GET requests (and any empty body) it is sha256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

TypeScript example

import { createHash, createHmac, randomBytes } from 'node:crypto';

const ALGORITHM = 'TEA1-HMAC-SHA256';

function sha256Hex(data: string | Buffer): string {
  return createHash('sha256').update(data).digest('hex');
}

function sign(secret: string, method: string, path: string, body = ''): Record<string, string> {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = randomBytes(16).toString('hex');
  const bodyHash = sha256Hex(body);
  const canonical = [ALGORITHM, method.toUpperCase(), path, '', timestamp, nonce, bodyHash].join('\n');
  const signature = createHmac('sha256', secret).update(canonical).digest('hex');
  return { timestamp, nonce, bodyHash, signature };
}

const body = JSON.stringify({ amount: 64400, duration_minutes: 60 });
const signed = sign(process.env.API_SECRET!, 'POST', '/api/v1/energy/quotes', body);

const res = await fetch('https://api.hexvetron.com/api/v1/energy/quotes', {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-api-key': process.env.API_KEY_ID!,
    'x-timestamp': signed.timestamp,
    'x-nonce': signed.nonce,
    'x-body-hash': signed.bodyHash,
    'x-signature': signed.signature,
  },
  body,
});

Replay protection

  • x-timestamp must be within the replay window of the server clock (a few minutes).
  • x-nonce is consumed server-side; the same nonce cannot be used twice within the window.
  • The body hash is re-verified against the raw bytes received, so a tampered body fails the signature check.

Request id

Optionally send an x-request-id header to correlate a response with your logs. It is echoed back in the envelope's request_id.