API Documentation

Base URL: https://zerotp.com  ·  also at https://api.zerotp.com without the /api prefix

Authentication

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)

recommended

Order 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.

GET/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"
    }
  ]
}
GET/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'
GET/api/v2/products

Price 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.

POST/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.

GET/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'
POST/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'
Cancelling has a short waiting period after the order is created — 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.
v2 error codes

Every failure returns {"status":"error","error":"<kode>","message":"…"}. Decide on error, not on the sentence.

HTTPerrorWhat to do
402insufficient_balanceTop up.
409no_stockThat combination or tier is empty right now. Try a higher tier or another country.
409price_above_maxRe-read /products; raise max_price if the new price is acceptable.
409price_changedThe list has corrected itself. Read it again and retry.
422too_earlyWait out cancel_in seconds, then cancel once.
429rate_limitedBack off, then retry.
503pool_unavailableThat pool cannot serve orders. Use another pool.
502upstream_errorTransient. Retry shortly.
GET/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
Catalogue notifications (opt-in)

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"
}
Off by default, deliberately. This goes to the same URL as your order callbacks but has a different shape — a receiver that assumes every payload has 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”.
Callback (Webhook)

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.

POST/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}'
One unpaid top up at a time. A new request is refused with 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.

GET/api/v1/services

Apps with their price and stock. (public)

curl https://zerotp.com/api/v1/services
POST/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}'
POST/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]}'
Response: 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.
GET/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'