Bitcall logoBitcall API
The Gateway

Conventions

The response envelopes, pagination, timestamps, money and request ids that every endpoint shares.

Every product behaves the same way here. Learn these once and the OTP, eSIM and HLR references only tell you what is inside data.

Two envelopes

A single resource, or the result of an action:

{ "data": { "orderId": "6a56174a…", "status": "waiting" } }

A list:

{
  "data": [  ],
  "meta": { "page": 1, "pageSize": 50, "total": 132, "totalPages": 3, "hasNext": true }
}

That is all. No bare arrays, no success flag, no ok. A 2xx always means it worked — you never have to read a boolean inside a success response to find out whether your request actually happened.

Pagination

Two query parameters, on every list endpoint, in every product:

ParameterDefaultRules
page1One-based. page=1 is the first page.
pageSize50Between 1 and 200.
curl 'https://api.khalil.chatup.ch/v1/otp/history?page=2&pageSize=25' \
  --header 'x-key-id: ak_live_1a2b…' \
  --header 'x-api-secret: 9f83…'

Out of range is refused, not trimmed. pageSize=5000 returns 400 VALIDATION_ERROR rather than quietly giving you 200 rows — silently truncated data looks like a complete answer and the mistake surfaces much later as missing records.

Page through with meta.hasNext rather than comparing counts yourself:

let page = 1;
for (;;) {
  const { data, meta } = await api('GET', '/v1/otp/history', { query: { page, pageSize: 100 } });
  handle(data);
  if (!meta.hasNext) break;
  page += 1;
}

Need a large completed HLR result set in one piece? Use the export endpoints rather than paging — they exist for exactly that.

Every response carries a request id

x-request-id: 9f1c7b2e-4a83-4d21-9c55-1f0b3a7e2d64

On success and on failure, and repeated inside the error body. Log it. It is the one thing that lets us find your exact request, and for the deliberately vague authentication errors it is the only way we can tell you what really failed.

We generate it ourselves; sending your own x-request-id has no effect.

Asynchronous work returns 202

Operations that continue after the response — an eSIM purchase, an HLR job, an export build — return 202 with something to poll:

{ "data": { "jobId": "3f2a…", "status": "queued" } }

Poll the resource until its status is terminal. Each product documents its own status vocabulary, and every one of them is a closed set.

Timestamps and money

Timestamps are ISO 8601 in UTC, always, in camelCase fields ending At:

{ "createdAt": "2026-08-01T09:59:00.000Z", "expiresAt": "2026-08-01T10:14:00.000Z" }

Money is an amount and a currency together, never a bare number:

{ "price": { "amount": 1.37, "currency": "USD" } }

Prices are what you pay. Our costs are not part of this API.

Rate limits

Exceeding your limit returns 429 RATE_LIMITED with a Retry-After header giving the seconds to wait. Honour it rather than retrying immediately — see Rate Limits.

What you will never see

Responses are built from an explicit list of public fields, so none of the following can appear, in any product:

  • database identifiers, or our internal ids for your resources
  • the names, aliases, references or raw payloads of the providers we buy from
  • our buying prices, charge records, balance references or billing states
  • your tenant or user identifiers
  • raw error text from an internal service

If you spot any of it, that is a bug worth reporting with the requestId.

On this page