Authentication
Authenticate requests to the gateway with your key id and secret.
Every request to a /v1/* endpoint is authenticated with two headers: your key id and your secret, both sent as-is. There is nothing to compute, sign, or time — a request you write once keeps working.
Required headers
| Header | Value |
|---|---|
x-key-id | Your key id, e.g. ak_live_… or ak_test_…. |
x-api-secret | Your secret — the 128-hex-character string returned at key creation. |
Each must be single-valued; duplicate or array-valued headers are rejected.
curl 'https://api.khalil.chatup.ch/v1/otp/history' \
--header 'x-key-id: ak_live_1a2b…' \
--header 'x-api-secret: 9f83…'That is the whole scheme. It works identically from curl, Postman, a browser, or any language.
The secret is displayed only at creation (and rotation) and is never retrievable again. Treat it like a password: keep it server-side, out of client-side code, out of version control, and out of screenshots. If it leaks, rotate the key — a leaked secret authorizes every endpoint your key's scopes allow, including purchases, until you do.
What the gateway checks, in order
Any failure stops the pipeline immediately:
- Both headers present.
- Key id is known and active.
- Source IP is allowed (if the key has an IP allowlist).
- Secret matches (constant-time comparison).
- Tenant exists and is not suspended/blocked.
- Key's role is high enough for the route.
- Product feature is enabled and the key holds the required scope.
- Rate limit not exceeded.
Sending no credentials returns 401 with error.code set to AUTHENTICATION_REQUIRED. Sending credentials that are not accepted returns 401 INVALID_API_KEY — and an unknown key, a wrong secret, a revoked key and a disallowed IP all look identical on purpose, so nobody can probe which key ids are real. See Errors.
The gateway checks the real network source address and ignores X-Forwarded-* headers. If you deploy it behind a proxy, terminate and forward correctly at the infrastructure layer.
Keeping a secret safe in transit
Because the secret travels on every request, it reaches anything that records request headers — reverse-proxy access logs, APM traces, WAF captures, your own shell history. Two habits matter more here than they would with a signed scheme:
- Use an IP allowlist. A key restricted to your servers' addresses is useless to anyone who reads it out of a log. Ask your Bitcall admin to set
allowedIpson production keys. - Rotate on any suspicion. Rotation is immediate and issues a new secret; the old one stops working at once.
HMAC signing (existing integrations)
The gateway also still accepts the earlier HMAC-signed form — x-key-id plus x-signature, x-timestamp, and x-nonce, where the signature is an HMAC-SHA256 over a canonical description of the request. Integrations built against it keep working unchanged, and nothing needs to migrate.
It remains the stronger option where you can afford the extra work: the secret never leaves your server, a captured request cannot be replayed (the nonce is single-use and the timestamp is bounded to ±5 minutes), and the signature commits to the exact method, path, query, and body. If you are integrating from a server you control and want that, ask us for the signing contract — it is unchanged.
New integrations should use the two-header form above.