OTP Overview
Rent numbers and receive one-time passcodes through the gateway.
The OTP product lets you rent a phone number for a specific service and country, then poll for the one‑time passcode that arrives on it.
All OTP endpoints are /v1/otp/* requests authenticated with your key id and secret — see Authentication. A key needs otp:read to read and otp:write to make changes; see Permissions.
Every endpoint, with its full request and response schema, is in the OTP API Reference.
A typical flow
Find the service and country
Browse either way round — services, or countries first:
await api('GET', '/v1/otp/services', { query: { q: 'whatsapp' } });
await api('GET', '/v1/otp/countries');Take the slug of the service you want (whatsapp) and the countryCode of the country (US_1).
Pick an offer, and take its serviceId
const { data } = await api('GET', '/v1/otp/services/whatsapp/US_1');
// data.pools is one entry per pool that sells this service here:
// [{ serviceId: 'CHARLIE-US-whatsapp', price: 0.42, availableCount: 12, successRate: 87 }, …]
const offer = data.pools[0];Each entry in pools is one offer — the same service and country, from a different pool, at its own price, availability and success rate. Choose one, and keep its serviceId: that single value is what you buy.
serviceId is opaque. It happens to read as pool‑country‑service, but do not assemble one yourself — the segments are spelled the way the selling side spells them, and a value you build will not resolve. Send back exactly what the catalog gave you.
Request a number
const { data: order } = await api('POST', '/v1/otp/request', {
body: { serviceId: offer.serviceId },
});This one spends money: send an Idempotency-Key header so a retry cannot rent a second number — it is required here, and a call without one is a 400 IDEMPOTENCY_KEY_REQUIRED. See Idempotency.
The response is the order: orderId, the rented phoneNumber, the price charged, and status: "waiting". serviceId comes back on it too, so a later order tells you which offer it was placed against — and you can re-request the same one.
Poll for the code
await api('GET', '/v1/otp/status');Poll until an order reaches received and read its code.
Cancel if unused
await api('POST', '/v1/otp/cancel', { body: { order_id: order.orderId } });The three identifiers
Nothing else in the OTP flow needs to be constructed — you only ever pass back a value you were given.
| Identifier | Where you get it | Where it goes |
|---|---|---|
serviceId | pools[].serviceId on any catalog offer | POST /v1/otp/request |
orderId | The order returned by request | cancel, resend, reactivate, and a refund request |
pool | GET /v1/otp/pools | The pool filter on GET /v1/otp/services. A pool is a grouping of numbers, not something you rent directly — offers are what you rent |
Every endpoint, with its parameters, response schema and error codes, is in the OTP API Reference.
Order statuses
An order is always in exactly one of these. They are the whole vocabulary — anything we
cannot map appears as unknown rather than as an internal name.
| Status | Meaning | What to do |
|---|---|---|
waiting | The number is rented and waiting for a code. | Poll. |
activating | The number is still being assigned. | Poll; treat as waiting. |
received | A code has arrived and is in code. | Read it. |
completed | The rental window closed with a code received. | Nothing. |
expired | The window closed without a code. | Request a new number, or reactivate. |
cancelled | You cancelled it. | Nothing. |
refunded | The charge was returned. | Nothing. |
unknown | We could not determine the state. | Poll; contact support with the requestId if it persists. |
Use actions on the order rather than inferring from the status — canResend,
canReactivate and canRefund already account for cooldowns and eligibility.
When things go wrong
| You see | Meaning | Do |
|---|---|---|
400 VALIDATION_ERROR | A field is wrong; fields says which. | Fix it. Retrying unchanged fails identically. |
409 PRODUCT_UNAVAILABLE | No number is available for that service and country right now. | Try another pool or country, or retry later. |
409 INSUFFICIENT_BALANCE | Not enough balance for the rental. | Top up, then retry with the same idempotency key. |
401 INVALID_API_KEY | The key is not accepted for this call — a wrong secret, or missing permissions. | Check the secret first, then the key's scopes with whoever issued it. |
404 NOT_FOUND | No such order — or not yours. | Check the id. The two cases are indistinguishable by design. |
Full request and response schemas are in the OTP API Reference; the shared rules are in Conventions and Errors.