HMAC-signed auth
Per-request HMAC signature auth used by Foxbit and other LATAM crypto exchanges — KEY/SECRET fields, validation round-trip, and runtime header injection.
HMAC-signed auth
@codespar/api-typesv0.4.0hmac_signed is the auth shape where every outbound request carries a freshly computed HMAC signature in addition to the public key — the credential is not a static bearer token but a per-call cryptographic proof that the caller knows the shared secret and the request body has not been tampered with.
This pattern is preferred by exchanges and any provider that wants replay resistance and tight binding between a key pair and the exact endpoint a request hits. The signature covers the timestamp, HTTP method, path, and body, so a captured request cannot be replayed against a different endpoint or after the timestamp window expires. It sits alongside api_key, path_secret, oauth, cert, and none in the six-type taxonomy.
When you'll use it
- Foxbit — production today. Brazilian crypto exchange. Trading + wallet endpoints.
- Future LATAM crypto exchanges that follow the same pattern via catalog overrides — Coinbase Pro, Kraken, and similar all use HMAC-signed APIs that fit the runtime once their per-provider header names and signing recipe are captured in the catalog row.
The defining property of these providers: they hand you a two-piece credential (a public access key and a private secret) and document a signing algorithm that mixes the secret with each request's data.
Field shape
The operator stores two values in the vault:
| Field | Visibility | What it is |
|---|---|---|
access_key | Visible (last-4 in the dashboard) | Public identifier of the key pair. Sent in cleartext on every request. Foxbit calls this KEY. |
secret | Masked entirely | Private signing secret. Never sent on the wire. Used only to compute the HMAC. Foxbit calls this SECRET. |
The dashboard's connect modal renders KEY/SECRET badges next to each input — visible-vs-masked is driven by the catalog row's per-field metadata, not hardcoded per provider, so a future provider that uses different terminology (api_key/api_secret, etc.) reuses the same component.
Connect flow
- Operator opens
/dashboard/auth-configs. - Picks the hmac_signed-typed server — for example,
foxbit. - The Provider Connect modal renders two fields with the right per-field badges (KEY visible, SECRET masked).
- Operator pastes both values from the provider's developer portal.
- Operator clicks Validate. The dashboard fires
POST /v1/connections/hmac-validate(admin-key route), which:- Computes a signature for a noop / echo request using the pasted secret.
- Sends that signed request to the provider's echo or healthz endpoint.
- Returns
{ ok: true }on a 2xx — or the provider's exact error code on failure (e.g.401 signature mismatch,401 key invalid).
- On a green response, operator clicks Save and both fields are persisted to the vault. On a red response, the dashboard surfaces the upstream error inline so the operator can fix the input before persisting anything.
Why validate before persist? A bad KEY/SECRET pair would otherwise sit silently in the vault until the first real session.execute() call, where the failure surfaces as a far less specific runtime error. The validation round-trip is fast, free, and trades one extra dashboard click for a clean operator UX.
What happens at runtime
On every outbound call to an hmac_signed provider, the proxy executor:
- Pulls
access_keyandsecretfrom the vault. - Builds the canonical signing string per the catalog row's recipe — for Foxbit that is
timestamp + method + path + body, wheretimestampis unix milliseconds. - Computes
signature = hmac_sha256(secret, signing_string). - Stamps the required headers onto the outbound request. For Foxbit:
X-FB-API-KEY,X-FB-API-TIMESTAMP,X-FB-API-SIGNATURE. For other providers, the header names are different — the catalog row records them.
session.execute("foxbit/order_create", { side: "buy", ... })
│
▼
proxy-executor.ts
│
├─ pull { access_key, secret } from vault
├─ ts = Date.now()
├─ signing_string = `${ts}${method}${path}${body}`
├─ sig = hmac_sha256(secret, signing_string)
├─ headers["X-FB-API-KEY"] = access_key
├─ headers["X-FB-API-TIMESTAMP"] = ts
├─ headers["X-FB-API-SIGNATURE"] = sig
▼
HTTPS request — provider verifies signature against the same recipe
│
▼
2xx → normalized response 4xx → bubbled up with provider error codeThe signature is recomputed on every request — there is no token to refresh, no session to extend. Replay protection comes from the timestamp: if the provider's clock and ours diverge by more than the provider's tolerance (typically 5 seconds), the request is rejected.
Catalog spec snippet
A minimal Foxbit catalog row declares the auth shape and the per-provider header mapping:
{
"id": "foxbit",
"name": "Foxbit",
"auth_type": "hmac_signed",
"secrets": [
{ "name": "access_key", "kind": "key", "label": "API key", "visibility": "visible" },
{ "name": "secret", "kind": "secret", "label": "API secret", "visibility": "masked" }
],
"hmac": {
"algorithm": "sha256",
"signing_string": "${timestamp}${method}${path}${body}",
"headers": {
"key": "X-FB-API-KEY",
"timestamp": "X-FB-API-TIMESTAMP",
"signature": "X-FB-API-SIGNATURE"
},
"timestamp_unit": "ms"
}
}The hmac block is the per-provider knob: change the header names, the concatenation order, the timestamp unit, or the algorithm and you've onboarded a different exchange without touching the runtime. Adding Coinbase Pro is a catalog drop, not a backend change.
Provider variations
Different exchanges signed the spec slightly differently — there is no industry standard, just a common shape:
- Header names vary widely. Foxbit prefixes with
X-FB-; Coinbase Pro usesCB-ACCESS-*; Kraken usesAPI-Key+API-Sign. - Signing-string concatenation order differs. Some providers put the body first, some put the path last, some include a passphrase. The catalog row's
signing_stringtemplate captures the exact order. - Timestamp units can be seconds or milliseconds — and a few use the
DateHTTP header instead of a custom one. - Algorithm is almost always SHA-256, but a couple use SHA-512.
The runtime never assumes; everything is read off the catalog row. When onboarding a new provider, the work is reading their docs carefully, transcribing the recipe into the catalog spec, and confirming with one validation round-trip.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 signature mismatch on every call | Clock skew (server time off > provider's tolerance) or wrong concatenation order in the catalog spec | Check NTP on the proxy host. Re-read the provider's docs and confirm signing_string matches their pseudocode exactly. |
401 key invalid immediately after Save | The KEY was pasted with a leading/trailing whitespace, or it was minted in a different environment (test vs prod) than the one being called | Re-mint or re-copy the key. Confirm the dashboard environment toggle (Live/Test) matches where the key is valid. |
Validate round-trip returns red, but Save would have succeeded | Almost never — the validation endpoint uses the same signing path as the runtime | Trust the red. Read the surfaced error code; it's the provider's exact response. |
Calls work for a few minutes then start failing 401 signature mismatch | Clock drift on the proxy host — common on virtualized infra without NTP | Enable NTP. Until then, intermittent 401s will keep happening. |
Next steps
Last updated on
mTLS / cert auth
How CodeSpar handles X.509 client-certificate authentication for BR open-banking corporate APIs — upload, runtime, rotation, expiry warnings.
Connect Links
Hosted OAuth flow that lets your end users connect their own Stripe, Mercado Pago, Shopify, and other provider accounts — without you building or maintaining a connection UI.