Signing reference
This page is the precise reference for the TEA1-HMAC-SHA256 request-signing scheme used on every customer API endpoint. It contains the canonical-string rules, frozen test vectors, and reference implementations in Python, Go, and Rust so you can validate your own client against a known answer.
For the narrative walkthrough and a TypeScript example, see Authentication.
Headers
Every signed request carries these five headers:
| Header | Value |
|---|---|
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 (16 random bytes in hex). |
x-body-hash | hex(sha256(rawBody)) — the hash of the exact request body; an empty body still hashes (sha256("")). |
x-signature | hex(HMAC-SHA256(secret, canonicalString)). |
Canonical string
The signature is an HMAC over the exact string below, joined with \n:
TEA1-HMAC-SHA256
<METHOD>
<path>
<canonicalQuery>
<timestamp>
<nonce>
<bodyHash>
<METHOD>— uppercased (GET/POST).<path>— the full path including the/api/v1prefix (e.g./api/v1/energy/quotes).<canonicalQuery>— the query string canonicalized as below; empty when there is no query string.<timestamp>,<nonce>,<bodyHash>— the corresponding header values.
Query canonicalization
- Split on
&, dropping empty segments. - Decode each key and value:
+→ space, then percent-decode. - Sort the pairs by key, then by value.
- Re-encode each key and value with RFC 3986 percent-encoding — only
A–Z a–z 0–9 - . _ ~are left literal (!becomes%21).
For example, page=2¬e=hello!world canonicalizes to:
note=hello%21world&page=2
Test vectors
The vectors below are frozen known answers — your client must reproduce them byte-for-byte. They use secret = sk_test_0123456789abcdef0123456789abcdef.
request-basic
| Field | Value |
|---|---|
| method | POST |
| path | /api/v1/energy/orders |
| rawQuery | (empty) |
| body | {"amount":64400,"duration_minutes":60} |
| timestamp | 1727520000 |
| nonce | 0123456789abcdef0123456789abcdef |
| bodyHash | 376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04 |
| signature | c749398f7d8b55d6b7b9e2332d5b5ee20006e5e950ba873f0f9077357973ded3 |
Canonical string:
TEA1-HMAC-SHA256
POST
/api/v1/energy/orders
1727520000
0123456789abcdef0123456789abcdef
376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04
Reference implementations
Each implementation reproduces the request-basic vector. Run it as-is and the self-check passes; for real requests, generate timestamp from the clock and nonce from a CSPRNG (os.urandom(16).hex() / crypto/rand / getrandom). Python and Go use only the standard library; Rust needs hmac = "0.12", sha2 = "0.10", hex = "0.4".
import hashlib, hmac, os, time from urllib.parse import quote, unquote_plus ALGORITHM = "TEA1-HMAC-SHA256" def rfc3986_encode(value): # Leave only RFC 3986 unreserved literal: A-Z a-z 0-9 - . _ ~ return quote(value, safe="-._~") def canonicalize_query(raw_query): if not raw_query: return "" pairs = [] for part in raw_query.split("&"): if not part: continue k, _, v = part.partition("=") pairs.append((unquote_plus(k), unquote_plus(v))) pairs.sort(key=lambda kv: (kv[0], kv[1])) return "&".join(f"{rfc3986_encode(k)}={rfc3986_encode(v)}" for k, v in pairs) def sign(secret, method, path, raw_query="", body="", timestamp=None, nonce=None): timestamp = timestamp or str(int(time.time())) nonce = nonce or os.urandom(16).hex() body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest() canonical = "\n".join([ALGORITHM, method.upper(), path, canonicalize_query(raw_query), timestamp, nonce, body_hash]) signature = hmac.new(secret.encode("utf-8"), canonical.encode("utf-8"), hashlib.sha256).hexdigest() return {"x-timestamp": timestamp, "x-nonce": nonce, "x-body-hash": body_hash, "x-signature": signature} # Self-check against the published vector headers = sign( "sk_test_0123456789abcdef0123456789abcdef", "POST", "/api/v1/energy/orders", body='{"amount":64400,"duration_minutes":60}', timestamp="1727520000", nonce="0123456789abcdef0123456789abcdef", ) assert headers["x-body-hash"] == "376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04" assert headers["x-signature"] == "c749398f7d8b55d6b7b9e2332d5b5ee20006e5e950ba873f0f9077357973ded3"
