Eko Platform Services Logo

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

KeyWhere it comes fromHow it's used
Access Key (also called Auth Key)Shared once, kept server-side only. Get UAT key from your dashboard; production key after KYCNever sent in a request; used to compute the secret-key
Developer KeyGet UAT key from your dashboard; production key after KYCSent as the developer_key header

Request headers

HeaderRequiredDescription
developer_keyyesYour static developer key.
secret-keyyesPer-request HMAC signature (see below).
secret-key-timestampyesCurrent time in milliseconds since the UNIX epoch.
content-typeyesapplication/json

Computing the secret-key

The signature binds the request to a timestamp, so it must be generated fresh for every call:

  1. Base64-encode the access_key — keep this as a string.
  2. Take the current timestamp in milliseconds, as a string.
  3. Compute HMAC-SHA256 with the timestamp as the message and that base64 string as the key.
  4. 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 a 401.

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):

  • 2483 Unauthorized — wrong developer_key, or one from the other environment.
  • 2484 Invalid 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.
  • 2485 Expired security headers — the timestamp is more than 2 minutes off our clock, or was reused. Sign every request fresh; sync your clock (NTP).
  • 2486 Invalid secret-key-timestamp — not in milliseconds (13 digits). Seconds (10 digits) is the usual mistake.
  • 2487 Missing security headers — secret-key or secret-key-timestamp not 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.

secret-key playground
base64( HMAC-SHA256( timestamp, base64(access_key) ) )
Runs in your browser

Send this exact value as secret-key-timestamp.

secret-key
Enter an access key and timestamp
secret-key-timestamp
Waiting for a valid timestamp
Verify an existing signature

Paste a signature above to compare it against the one computed from the same access key and timestamp.

Warning

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.