API Documentation
Base URL: https://zerotp.com
· also at https://api.zerotp.com without the /api prefix
Send the X-API-KEY header on every request. One key for every API version, and one shared balance.
Sign in to see your API key.
Global Numbers API (v2)
recommendedOrder a number by service + country. One combination can have several price tiers: a higher tier opens a larger pool of numbers, so your chance of getting one is higher — the number itself is not better.
Suppliers are identified only by anonymous pool codes (p1, p2, …). Those codes are stable and safe to store; supplier names never appear in any response.
/api/v2/services
Services that currently have stock, best sellers first. Every row carries an icon — served from our CDN, free to use directly in your own interface (open CORS, cached 7 days). Always present. (public)
curl https://zerotp.com/api/v2/services
{
"status": "success",
"data": [
{
"service": "whatsapp",
"name": "Whatsapp",
"icon": "https://zerotp.com/cdn/icons/whatsapp.webp"
}
]
}
/api/v2/countries
Countries that have stock. Add ?service=whatsapp to narrow it to one service — only countries you can actually order from are listed. (public)
curl 'https://zerotp.com/api/v2/countries?service=whatsapp'
/api/v2/productsPrice list for one service + country, cheapest first. This is what you choose from. The numbers are fetched live from the supplier on request (cached 15 seconds), so price and stock are current.
curl 'https://zerotp.com/api/v2/products?service=whatsapp&country=id' -H 'X-API-KEY: YOUR_KEY'
{
"status": "success",
"data": {
"service": "whatsapp",
"country": "id",
"products": [
{ "pool": "p2", "tier": 1, "tier_label": "saver", "price": 2500, "stock": 3530, "success_rate": null },
{ "pool": "p2", "tier": 2, "tier_label": "standard", "price": 2800, "stock": 40656, "success_rate": null },
{ "pool": "p2", "tier": 5, "tier_label": "max", "price": 14900, "stock": 1297037, "success_rate": null }
]
}
}
tier — tier number, used when ordering. tier_label: saver (cheapest),
standard, max (largest pool).
stock — how many numbers are reachable at that tier. This is what separates the tiers, not the quality of the number.
success_rate — measured from real orders on this platform, and stays null while the sample is too small to mean anything.
/api/v2/orders
Body: service and country (required); pool, tier, max_price, custom_user_id (optional).
curl -X POST https://zerotp.com/api/v2/orders \
-H 'X-API-KEY: YOUR_KEY' -H 'Content-Type: application/json' \
-d '{"service":"whatsapp","country":"id","tier":2,"max_price":2800}'
max_price is a promise, not a filter. If the current price is above it, the order is refused with 409 price_above_max and nothing is charged — so a price change between reading /products and ordering can never bill you more than you agreed to. Send the price you were shown.
pool and tier are honoured literally. If what you named has no stock, you get 409 no_stock — you are never moved silently to another pool or tier, because that is exactly what you were choosing.
Without pool/tier, the cheapest one is used.
/api/v2/orders/{order_id}
Status and OTP (otp_code, sms_text, sms_history). GET /api/v2/orders lists your orders; GET /api/v2/balance shows your balance.
curl https://zerotp.com/api/v2/orders/260918041233 -H 'X-API-KEY: YOUR_KEY'
/api/v2/orders/{order_id}/cancel / /finish / /resend
cancel refunds in full as long as no SMS has arrived. finish closes an order you are done with. resend asks for another SMS on the same number.
curl -X POST https://zerotp.com/api/v2/orders/260918041233/cancel -H 'X-API-KEY: YOUR_KEY'
can_cancel and cancel_in on the order tell you exactly when. Calling early returns 422 too_early, and retrying in a loop can make the number impossible to cancel at all — some suppliers restart the countdown on every refusal. Wait out cancel_in, then call once.
Every failure returns {"status":"error","error":"<kode>","message":"…"}.
Decide on error, not on the sentence.
| HTTP | error | What to do |
|---|---|---|
| 402 | insufficient_balance | Top up. |
| 409 | no_stock | That combination or tier is empty right now. Try a higher tier or another country. |
| 409 | price_above_max | Re-read /products; raise max_price if the new price is acceptable. |
| 409 | price_changed | The list has corrected itself. Read it again and retry. |
| 422 | too_early | Wait out cancel_in seconds, then cancel once. |
| 429 | rate_limited | Back off, then retry. |
| 503 | pool_unavailable | That pool cannot serve orders. Use another pool. |
| 502 | upstream_error | Transient. Retry shortly. |
/api/v2/catalog
know when to re-fetch
The service list changes whenever a new app is listed. Rather than re-downloading it on a timer,
poll this: the response is a few hundred bytes, so checking every minute costs nothing.
version only moves when the set of sellable services actually changes — not when
stock or prices move — so a stable version really does mean nothing new.
curl https://zerotp.com/api/v2/catalog
{
"status": "success",
"data": {
"version": "8f2fb5c01ba24e7f",
"changed_at": "2026-09-18T06:59:26+00:00",
"new_services": [
{ "service": "newapp", "name": "New App", "icon": "https://zerotp.com/cdn/icons/newapp.webp" }
]
}
}
Cheaper still: use the ETag. /api/v2/services and /api/v2/countries
return an ETag and an X-Catalog-Version header. Send the ETag back as
If-None-Match and you get 304 Not Modified with an empty body instead of
100 KB of JSON.
curl https://zerotp.com/api/v2/services -H 'If-None-Match: "8f2fb5c01ba24e7f"' # HTTP/2 304 — 0 bytes
If you would rather be told than poll, turn on “Also notify me when new services are listed” under API & Callback. We then POST this to your callback URL whenever new services appear:
{
"event": "catalog.updated",
"version": "8f2fb5c01ba24e7f",
"changed_at": "2026-09-18T06:59:26+00:00",
"new_services": [
{ "service": "newapp", "name": "New App", "icon": "…" }
],
"refresh": "https://zerotp.com/api/v2/services"
}
order_id would trip over it.
Switch it on only once your receiver checks the event field first; order callbacks do not
carry that field, so event present means “not an order”.
If callbacks are enabled under the API & Callback menu, the system will POST JSON to your URL as soon as an OTP arrives:
{
"order_id": 260918041233,
"pool": "p2",
"service": "whatsapp",
"country": "id",
"phone_number": "628xxx",
"otp_code": "123456",
"sms_text": "Your code is 123456",
"status": "completed"
}
v1 orders use the same URL and the same shape without pool/service/country — a receiver written for either one works for both.
/api/v1/deposits
top up
Top up without opening the dashboard. Body: amount in rupiah (min 10,000, max 10,000,000). The response carries the QRIS — qr_string, qr_image (PNG data-URI) and payment_url. It serves both API versions: there is only one balance.
Two amounts: pay_amount is what the payer must transfer — amount plus the QRIS fee (e.g. amount 10,000 → pay_amount 10,257). amount is what lands in your balance. Show pay_amount to whoever scans the QR.
curl -X POST https://zerotp.com/api/v1/deposits \
-H 'X-API-KEY: YOUR_KEY' -H 'Content-Type: application/json' \
-d '{"amount": 50000}'
409 pending_exists and the blocking one is returned in data — pay it, or cancel it with POST /api/v1/deposits/{reference}/cancel. Balance is credited when the payment actually settles, not when the QR is created. Check status with GET /api/v1/deposits/{reference}.
Fleet API (v1)
legacy
Sells our own modem fleet — Indonesia only, addressed by application_id. Still supported exactly as documented and unchanged; existing integrations need no work. Only here: multi-service, several apps on one number. For the multi-country catalogue, use v2 above.
/api/v1/servicesApps with their price and stock. (public)
curl https://zerotp.com/api/v1/services
/api/v1/orders
Body: application_id (required), custom_user_id (optional). Single price tier — any operator field you send is ignored.
curl -X POST https://zerotp.com/api/v1/orders \
-H 'X-API-KEY: YOUR_KEY' -H 'Content-Type: application/json' \
-d '{"application_id": 1}'
/api/v1/orders
multi-service
Send application_ids to put several apps on one shared number — for accounts that must share a phone identity. Each app is charged separately and stays its own order, so status, cancel and finish work per app.
curl -X POST https://zerotp.com/api/v1/orders \
-H 'X-API-KEY: YOUR_KEY' -H 'Content-Type: application/json' \
-d '{"application_ids": [12, 73]}'
group_id, phone_number, total_price, orders[]. Apps that accept SMS from the same sender are refused with sender_conflict — one OTP could otherwise land on the wrong order.
/api/v1/orders/{order_id} ·
POST/cancel / /finish
Check status & OTP, cancel (refunds if no SMS yet), or mark as finished. GET /api/v1/balance shows your balance; pending is money held by running orders — not spent, but not spendable either.
curl https://zerotp.com/api/v1/orders/123 -H 'X-API-KEY: YOUR_KEY'