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 shown when the key was issued. |
curl 'https://api.bitcall.io/v1/otp/activations' \
--header 'x-key-id: ak_live_1a2b…' \
--header 'x-api-secret: 9f83…'Each header must be single-valued. A duplicate or array-valued header is ignored, which then reads as if it were absent.
That is the whole scheme. It works identically from curl, Postman, a browser, or any language.
const res = await fetch('https://api.bitcall.io/v1/esim/plans?region=Europe&pageSize=50', {
headers: {
'x-key-id': process.env.KEY_ID,
'x-api-secret': process.env.SECRET,
},
});A POST adds only a body and its content type:
const res = await fetch('https://api.bitcall.io/v1/esim/orders', {
method: 'POST',
headers: {
'x-key-id': process.env.KEY_ID,
'x-api-secret': process.env.SECRET,
'Content-Type': 'application/json',
},
body: JSON.stringify({ planId: 'pln_…' }),
});The secret is displayed only when the key is issued or rotated, and cannot be read back afterwards — not in the panel, not through the API, and not by a support request. Treat it like a password: keep it server-side, out of client-side code, out of version control, and out of screenshots. A leaked secret authorizes every endpoint your key can reach — purchases included — until the key is rotated.
Keeping your secret safe
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. Nothing binds the credential to a particular path or body, so a secret read out of a log is enough to call anything your key can call.
Two controls carry the weight:
- Use an IP allowlist. A key restricted to your servers' addresses is useless to anyone who reads the secret out of a log somewhere else. Set one when you create the key in your panel — see Get & Manage Keys. It narrows where a stolen secret can be used, not whether it works: a request from an allowlisted address is accepted whoever sends it, so it does not protect you against a leak on — or a foothold inside — one of those hosts.
- Rotate on any suspicion. Rotation is immediate and has no overlap window: a new key id and secret are issued and the old key stops authenticating at once, so calls still carrying the old credential fail until you deploy the new one. On a suspected leak that gap is worth taking. For a planned credential change, deploy a second key first and revoke the old one afterwards instead — see Rotating a key.
Keep the secret on a server you control. A request made from a browser or a mobile app ships the secret to the device, where any user can read it.
Using the interactive reference
Each API reference page can send real requests with your own credentials. What happens to them is worth knowing before you paste anything in:
- They are held in the page's memory only — no
localStorage, nosessionStorage, no cookie — and are discarded when you close or reload the tab. - They are not sent only to the gateway. Every request goes through this documentation site's own server, which relays it upstream; the gateway sends no CORS headers, so a direct call from your browser would be blocked. That server receives your credential in transit. It stores no copy and writes none to its logs, but it does see it.
- Requests are real. A purchase there charges your account exactly as it would from your own code, so each mutating call asks you to confirm first.
Prefer a key issued for a non-production environment here, and confirm with your provider what that environment actually costs — see Environments and billing.
What the gateway checks, in order
Any failure stops the request immediately — it is never forwarded to a product:
- Both headers present. Missing credentials →
401 AUTHENTICATION_REQUIRED. - Key id is known and active.
- Source IP is allowed, if the key has an IP allowlist.
- Secret matches, compared in constant time.
- Account is in good standing.
- The product is enabled for your account and your key may call this endpoint.
- Rate limit not exceeded — see Rate Limits.
- The request is well-formed. Path, query and body are checked against the endpoint's
schema. A bad request →
400 VALIDATION_ERROR, withfieldsnaming what was wrong.
Credentials are checked before the request itself, so a 400 means your key was
accepted. A request with bad or missing credentials answers 401 whatever its body,
path or query. If you get 400 VALIDATION_ERROR, your key works: fix the fields it names.
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 IP allowlist is checked against the address the gateway sees. If you call from behind a proxy or NAT, allowlist the egress addresses your traffic actually leaves from, not your internal ones.