Bitcall logoBitcall API
The Gateway

Errors

One error shape, thirteen codes, and what to do about each.

Every failure — from any product, at any layer — has the same shape:

{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Your account balance is too low for this operation.",
    "requestId": "9f1c7b2e-4a83-4d21-9c55-1f0b3a7e2d64"
  }
}

Branch on code, never on message. The code is contract; the message is prose we may improve. requestId is the same value as the x-request-id response header — quote it when you contact support and we can find your exact request.

fields appears on validation failures only, naming the offending fields without repeating their values:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request is not valid. See `fields` for what to correct.",
    "requestId": "…",
    "fields": ["body.numbers", "query.pageSize"]
  }
}

The codes

These thirteen are the whole vocabulary. Nothing else is ever returned — and note there is no 403: a call your key is not allowed to make answers 401, like any other credential problem.

Eleven are shared by every product. The last two — CANCELLATION_NOT_AVAILABLE_YET and REACTIVATION_UNAVAILABLE — are returned only by the OTP cancel and reactivate operations; you will never see them elsewhere, but they are part of the same Error schema, so branch on them if you call those operations.

Your request needs changing

CodeHTTPWhat to do
VALIDATION_ERROR400Fix the fields listed in fields. Retrying unchanged will fail identically.
NOT_FOUND404The identifier does not exist, or is not yours. See below.
CONFLICT409The resource is not in a state that allows this — an order already cancelled, a job already finished.

Your credentials

CodeHTTPWhat to do
AUTHENTICATION_REQUIRED401You sent no credentials on an otherwise valid request. Add x-key-id and x-api-secret. (An invalid request answers VALIDATION_ERROR first, whatever you sent.)
INVALID_API_KEY401The credentials you sent were not accepted.

INVALID_API_KEY is deliberately one answer to several situations: a key that does not exist, a wrong secret, a revoked key, an IP outside the key's allowlist, and a key that is not allowed to make this particular call. Distinguishing them would let someone probe which key ids are real without holding a secret.

So if your credentials are definitely correct, check that the key is still active and that you are calling from an allowlisted address. Quote the requestId and support can say exactly which check failed.

Something is unavailable

CodeHTTPWhat to do
INSUFFICIENT_BALANCE409Top up. Retrying without doing so will fail identically.
PRODUCT_UNAVAILABLE409This product cannot be sold right now. Try another, or retry later.
CANCELLATION_NOT_AVAILABLE_YET409(OTP cancel only) You asked to cancel a number before the provider's cooldown elapsed. Wait, then cancel again — the block lifts on its own.
REACTIVATION_UNAVAILABLE409(OTP reactivate only) The number cannot be reactivated — its window has closed. Do not retry; request a new number instead.
RATE_LIMITED429Wait for the interval in the Retry-After header, then retry.
SERVICE_UNAVAILABLE503A dependency is temporarily down. Retry with backoff.
SERVICE_RESPONSE_INVALID502A downstream service answered in a way that does not match our contract. We rejected it rather than pass it on. Retry; if it persists, report it with the requestId.
INTERNAL_ERROR500Our bug. Retry once, then report it with the requestId.

Which errors are worth retrying

BehaviourCodes
Do not retry — the outcome will not changeVALIDATION_ERROR · NOT_FOUND · CONFLICT · AUTHENTICATION_REQUIRED · INVALID_API_KEY · REACTIVATION_UNAVAILABLE
Retry after actingINSUFFICIENT_BALANCE (top up) · RATE_LIMITED (wait for Retry-After) · CANCELLATION_NOT_AVAILABLE_YET (wait out the cooldown)
Retry with backoffSERVICE_UNAVAILABLE · SERVICE_RESPONSE_INVALID · INTERNAL_ERROR · PRODUCT_UNAVAILABLE

Retrying an operation that spends money can spend it twice. A purchase, a top-up and an OTP rental take effect on every call, and there is no key or header that makes a retry safe.

If one of those requests times out or fails in a way that leaves the outcome unknown, do not resend it blind. Read the resource back first — list your orders, or poll the order the first attempt would have created — and only retry once you know nothing was created. x-request-id from the failed attempt lets us confirm what happened if you cannot tell.

Exception — an HLR batch submission (POST /v1/hlr/lookups, many numbers) is idempotent. Re-sending the identical batch — while it runs, or within ~1 minute of it completing — replays the SAME lookup (same jobId) and never charges twice, so a lost response is safe to retry as-is. A replay returns the job, not a fresh inline result — read the outcome from GET /v1/hlr/lookups/{lookupId} and its /results.

A single lookup (one number) is not deduplicated. Every POST performs a fresh, separately-billed lookup by design. Do not blind-retry one to recover a dropped response — you would be charged again. If a single lookup's response is lost, find it with GET /v1/hlr/lookups (your recent lookups) instead of re-posting. For every other money-spending create, the rule above still holds.

A 404 never confirms someone else's data

If you ask for an identifier that belongs to another account, you get exactly the same 404 NOT_FOUND as for an identifier that has never existed. The two are indistinguishable on purpose: an answer that distinguished them would confirm that a guessed order id is real and belongs to somebody.

So a 404 means "not yours, or not a thing" — never "exists, but you may not see it".

On this page