How Authentication Works
Every Eko API request is authenticated with a static key plus a per-request HMAC signature. Nothing secret ever travels on the wire in plain form.
All traffic runs over TLS 1.2+, and production access can additionally be locked to your server's static IP (optional IP whitelisting). See Security & data protection for the full picture.
The two keys
| Key | Where it comes from | How it's used |
|---|---|---|
| Access Key (also called Auth Key) | Shared once, kept server-side only. Get UAT key from your dashboard; production key after KYC | Never sent in a request; used to compute the secret-key |
| Developer Key | Get UAT key from your dashboard; production key after KYC | Sent as the developer_key header |
Request headers
| Header | Required | Description |
|---|---|---|
developer_key | yes | Your static developer key. |
secret-key | yes | Per-request HMAC signature (see below). |
secret-key-timestamp | yes | Current time in milliseconds since the UNIX epoch. |
content-type | yes | application/json |
Computing the secret-key
The signature binds the request to a timestamp, so it must be generated fresh for every call:
- Base64-encode the
access_key— keep this as a string. - Take the current timestamp in milliseconds, as a string.
- Compute
HMAC-SHA256with the timestamp as the message and that base64 string as the key. - Base64-encode the result — that's the
secret-key.
Send the same timestamp in secret-key-timestamp. It must be within 2
minutes of our server clock (either direction) and must not be reused from an
earlier request — so generate a new timestamp and secret-key for every call.
secret-key = base64( HMAC-SHA256( message = timestamp, key = base64(access_key) ) )
Common mistake: the HMAC key is the base64 string
base64(access_key), used as-is. Do not base64-decode it back to raw bytes before signing — that produces a different signature and a401.
Code examples
Sign on your backend — the access_key must never reach a browser. The same
recipe in five languages:
// Backend only. Never expose access_key in a browser.import crypto from "node:crypto";const accessKey = process.env.EKO_ACCESS_KEY;const timestamp = Date.now().toString();const encodedKey = Buffer.from(accessKey).toString("base64");const secretKey = crypto.createHmac("sha256", encodedKey).update(timestamp).digest("base64");// headers: { developer_key, "secret-key": secretKey, "secret-key-timestamp": timestamp }
Troubleshooting a 401
A failed authentication returns HTTP 401. The response body's status names
the check that failed (full table in
Authentication errors):
2483Unauthorized — wrongdeveloper_key, or one from the other environment.2484Invalid secret-key or timestamp — the signature doesn't match. Check the timestamp you signed is the one you sent, and verify your code in the playground below.2485Expired security headers — the timestamp is more than 2 minutes off our clock, or was reused. Sign every request fresh; sync your clock (NTP).2486Invalid secret-key-timestamp — not in milliseconds (13 digits). Seconds (10 digits) is the usual mistake.2487Missing security headers —secret-keyorsecret-key-timestampnot sent, or the header name is misspelled.
Signing checks out but still 401? Check the base URL matches your keys (UAT
vs production) and, in production, that your server's IP is whitelisted.
Try it: secret-key playground
Paste an access key and a timestamp to see the exact secret-key those inputs
produce — or paste a signature your own code generated and check it against
ours to find out why a request is coming back 401.
Send this exact value as secret-key-timestamp.
Verify an existing signature
Paste a signature above to compare it against the one computed from the same access key and timestamp.
This playground signs entirely inside your browser with the Web Crypto API —
your access key is never sent to Eko or stored anywhere. That is safe for a
scratchpad, but not a pattern to copy: in a real integration the access_key
must stay on your backend, because anything shipped to a browser is public.