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
| Header | Description |
|---|---|
x-api-key | Your access key id (public). |
x-timestamp | Unix time in seconds when the request was created. |
x-nonce | A random string, unique per request (e.g. 16 random bytes in hex). |
x-body-hash | hex(sha256(rawBody)) — the hash of the exact request body. |
x-signature | hex(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/v1prefix — 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
- The path includes
/api/v1. Sign/api/v1/energy/quotes, not/energy/quotes. x-body-hashis never omitted. ForGETrequests (and any empty body) it issha256("")=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-timestampmust be within the replay window of the server clock (a few minutes).x-nonceis 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.
