---
title: "How Authentication Works"
description: "Keys, request signing, and the per-request HMAC secret-key."
canonical: "https://eps.eko.in/docs/how-auth-works"
---


> **Canonical URL:** https://eps.eko.in/docs/how-auth-works
> This is a machine-readable Markdown version of the page for AI agents and LLMs. The primary (HTML) version lives at the canonical URL above.

# 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](/security) 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:

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:

```javascript
// 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](/docs/error-codes#authentication-errors-http-401)):

- **`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](#try-it-secret-key-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`.

> **Interactive secret-key playground** — available on the HTML version of this page (https://eps.eko.in/docs/how-auth-works). It computes and verifies the signature in the browser, so it has no markdown equivalent.

> 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.
