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 },
});Each call to this endpoint rents a number and charges for it. If it times out or fails without a clear answer, do not resend it — read your orders back to see whether one was created, and only then decide whether to try again.
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
const { data } = await api('GET', '/v1/otp/status', {
query: { orderId: order.orderId },
});Poll until the order reaches received, then read its code.
orderId narrows the answer to that one order. Leave it off and you get all of
them, newest first — useful for a dashboard, wasteful for a single rental.
The response is a list either way, so a filtered poll is data[0]. An order
that has not been placed, or that belongs to someone else, is simply not in the
list — there is no separate "not found" to handle.
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 | The orderId filter on status, plus cancel, resend and reactivate |
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. |
An order reaches refunded on its own — cancelling a number before its code arrives
returns the rental. There is no refund endpoint to call.
Use actions on the order rather than inferring from the status — canResend and
canReactivate 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 send the request again. Nothing was charged, so no number was rented. |
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.