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.
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. A stale or mismatched
timestamp returns 403 Forbidden.
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 a403.
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 }
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 403.
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.