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:

HeaderValue
x-api-keyYour access key id (public).
x-timestampUnix time in seconds when the request was created.
x-nonceA random string, unique per request (16 random bytes in hex).
x-body-hashhex(sha256(rawBody)) — the hash of the exact request body; an empty body still hashes (sha256("")).
x-signaturehex(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/v1 prefix (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

  1. Split on &, dropping empty segments.
  2. Decode each key and value: + → space, then percent-decode.
  3. Sort the pairs by key, then by value.
  4. 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&note=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

FieldValue
methodPOST
path/api/v1/energy/orders
rawQuery(empty)
body{"amount":64400,"duration_minutes":60}
timestamp1727520000
nonce0123456789abcdef0123456789abcdef
bodyHash376b74a8d3e3b9fc48fcd6f49374a0ed495b7ac63dc285037376f55d54120e04
signaturec749398f7d8b55d6b7b9e2332d5b5ee20006e5e950ba873f0f9077357973ded3

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"