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:
| Parameter | Default | Rules |
|---|---|---|
page | 1 | One-based. page=1 is the first page. |
pageSize | 50 | Between 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-1f0b3a7e2d64On 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.