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.

One exception: GET /v1/balance. It returns its object at the top level — { "balance": …, "reserved": …, "currency": "…" } — not wrapped in data. It is a single, always-present account fact with no id and no list form, so it is read directly. Every other endpoint follows the envelopes above.

Pagination

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

ParameterDefaultRules
page1One-based. page=1 is the first page.
pageSize50Between 1 and 200.

One list is not paginated: GET /v1/hlr/products returns your whole product catalogue in a single response — a handful of rows — and takes neither parameter. It still carries the same meta block, with hasNext: false, so code that reads meta uniformly keeps working. Every other list in every product paginates. The reference is authoritative per endpoint: if page is not in an operation's parameters, it does not paginate.

curl 'https://api.bitcall.io/v1/otp/activations?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/activations', { 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.

Every amount is in your account's currency — the one your wallet settles in, shown on each price and on GET /v1/balance. It is an account fact, not a per-request choice: there is no currency parameter to ask for another one, and prices always come back in your settlement currency.

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
  • the internal account and user identifiers your key resolves to
  • 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