Developers
SMS verification API
Everything the dashboard does, from your own software: check prices, buy a number, read the code, cancel for a refund. Same balance, same prices, one endpoint.
Getting started
- Create an account and top up your balance.
- In Settings → API, request API access and say what you will use it for. Access is approved per account.
- Once approved, create a key there. It starts with
nsk_live_and is shown once: store it in an environment variable, never in a URL or in browser code.
Every request is a POST https://ninjasms.shop/api/sms with the key in Authorization: Bearer … and a JSON body { "operation": "…", "input": { … } }. Success is { ok: true, data }; a refusal is an HTTP error with { error, code? }. Prices are in US dollars.
Examples
Balance
curl -s https://ninjasms.shop/api/sms \
-H "Authorization: Bearer $NSK_KEY" \
-H "Content-Type: application/json" \
-d '{"operation":"balance.read"}'Returns { ok: true, data: { balance } }.
Prices
curl -s https://ninjasms.shop/api/sms \
-H "Authorization: Bearer $NSK_KEY" \
-H "Content-Type: application/json" \
-d '{"operation":"catalog.prices","input":{"service":"wa","country":2}}'Your price and stock per country and service: data[country][service] = { price, count }. Omit country for every country.
Buy a number
curl -s https://ninjasms.shop/api/sms \
-H "Authorization: Bearer $NSK_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a4e-order-1042" \
-d '{"operation":"activation.purchase","input":{"service":"wa","country":2,"expectedPrice":0.13}}'Send the price you quoted as expectedPrice, as a number or a numeric string: if the price has risen past it the purchase is refused with 409 price_changed and nothing is charged. Leave expectedPrice out and the check is skipped, so you are charged the current price; a value that is not a number is refused with 400 bad_expected_price. "operator" is optional: leave it out to buy on any mobile operator. An Idempotency-Key is required (400 idempotency_key_required without one): use one per order and keep it for every retry of that order — a retry with the same key never buys a second number. Only drop the key after a success or a 409 already_completed, which includes the number that order bought.
Check for the code
curl -s https://ninjasms.shop/api/sms \
-H "Authorization: Bearer $NSK_KEY" \
-H "Content-Type: application/json" \
-d '{"operation":"activation.status","input":{"id":"123"}}'Use the id from the purchase response. Poll every few seconds until the code arrives.
Cancel
curl -s https://ninjasms.shop/api/sms \
-H "Authorization: Bearer $NSK_KEY" \
-H "Content-Type: application/json" \
-d '{"operation":"activation.transition","input":{"id":"123","transition":"cancel"}}'Refunds the number. Cancelling opens 2 minutes after purchase and closes once a code has arrived. Use "complete" once you have the code.
Retries never buy twice
Every purchase needs an Idempotency-Key header, one per order (a UUID or your own order number). If a request times out or your connection drops, send the same request with the same key: you get the number that order already bought, never a second one.
Keep the key until the outcome is final: a success, or 409 already_completed. On 409 in_progress wait and retry with the same key; on 409 uncertain stop and contact support, keeping the key. After price_changed, no_numbers_at_price or a 402, nothing was charged — re-read the price and retry with the same key.
Send the price you were shown as expectedPrice. If the price has risen past it, the purchase is refused and nothing is charged, so you are never billed more than you agreed to. The value may be a number or a numeric string; if you leave it out, that check is skipped and you are charged the current price.
Operations
| Operation | Input | What it does |
|---|---|---|
| balance.read | — | Your balance. |
| catalog.services | — | The services on sale, with their codes. |
| catalog.countries | — | The countries, with their ids. |
| catalog.prices | { service, country?, type?, duration? } | Your price and stock. type is sms (default), call or rent; a rental also needs duration in hours. |
| activation.purchase | { service, country, expectedPrice, operator?, type?, duration?, phoneException? } | Buy one number. Needs an Idempotency-Key header. For several numbers, send several purchases, each with its own key. |
| activation.status | { id } | One number and its code, once it has arrived. |
| activation.messages | { id } | Every SMS a number received, newest first (a rental keeps receiving until it ends). |
| activation.transition | { id, transition: cancel | complete } | Cancel for a refund, or complete once you have the code. |
| activation.reactivation.quote | { id } | Price of a second code on a number that already received one. |
| activation.reactivation.purchase | { id, expectedPrice } | Buy that second code. Same Idempotency-Key and price rules as a purchase. |
| activations.list | — | Your numbers that are still waiting or active. |
| history.list | { size ≤ 100, offset } | Your past numbers, newest first. |
Webhooks
Instead of polling for codes, set a webhook under Settings → API. Each event is POSTed to your https endpoint as JSON the moment it happens: activation.code (activation_id, service, phone, code) and deposit.credited (reference, amount).
Every request carries X-Webhook-Signature: t=<unix>,v1=<hex>: an HMAC-SHA256 of t.body with your signing secret. Check it, and reject a timestamp older than five minutes, before trusting the event. Answer 2xx within five seconds; a failed event is retried after 1, 5, 15, 60 and 180 minutes with the same id, so drop repeats. Ten failures in a row, or a 410, pause the webhook.
Errors
| Status | Code | What to do |
|---|---|---|
| 400 | idempotency_key_required | A purchase without an Idempotency-Key header. Add one per order. |
| 401 | invalid_api_key | The key is missing, revoked, or was revoked by a password change. Create a new one. |
| 402 | — | Not enough balance for this number. Top up and retry with the same key. |
| 403 | ip_not_allowed | The key is restricted to certain IP addresses and this request came from another. Call from an allowed address, or change the list in Settings → API. |
| 403 | api_access_inactive | API access is not active for the account. Request it in Settings → API. |
| 400 | bad_expected_price | expectedPrice is not a number. Send the price you were quoted, or leave it out. |
| 400 | unknown_service | The service code is not a valid code. Use a code from catalog.services. |
| 409 | no_code_yet | A number can be completed only after its code arrives. Wait for the code, or cancel it for a refund. |
| 403 | not_available | That service or country is not sold for this number type. Nothing was charged. |
| 409 | price_changed | The price rose above your expectedPrice. Nothing was charged: re-read the price, then retry with the same key. |
| 409 | in_progress | This Idempotency-Key is still being worked on. Do not buy again: wait a few seconds and retry with the same key, or check your active numbers. |
| 409 | uncertain | The outcome of this purchase is not known yet. Stop: do not retry, keep the key, and contact support with the recovery_reference in the response. Your balance is held safely. |
| 409 | already_completed | This Idempotency-Key already bought a number; the response carries it. Do not buy again. |
| 409 | no_numbers_at_price | Numbers at that price ran out. Re-read the price list and try again. |
| 409 | refund_window_closed | A rental can only be cancelled for a refund within 20 minutes and before any SMS. |
| 425 | — | A number cannot be cancelled in its first two minutes. The message says how many seconds are left; retry after that. |
| 429 | — | Too many requests. Wait for the Retry-After seconds, then retry. |
| 503 | maintenance | Planned maintenance is pausing purchases and deposits. Nothing was charged. Retry after the Retry-After seconds with the same key. |