API Reference

A REST API over HTTPS with JSON bodies and Bearer authentication. Buy a virtual number for an exact price tier, poll it for the verification code, then finish. Everything the dashboard does is available here.

Overview

The API is versioned under /api/v2. Every response is JSON with a success boolean at the top — see Response format. Money is a decimal USD string with variable precision ("0.5", "0.425") — parse it, don't assume two places. Timestamps are ISO 8601 UTC.

Typical integration: list products for a service, create an order against one, then poll the order until otp_code is set.

Base URL

FieldTypeNotes
Productionstringhttps://api.smsgecko.com/api/v2

Authentication

Every request needs a Bearer token. Mint one from the dashboard (Settings → API keys) using a session cookie. Keys look like smsg_live_… and are shown only once, so copy it somewhere safe and include it in every request.

Header
curl https://api.smsgecko.com/api/v2/orders/active \
  -H "Authorization: Bearer smsg_live_xxxxxxxxxxxx"

A missing or invalid key returns 401 UNAUTHORIZED. A valid key for a suspended account returns 403 FORBIDDEN.

Response format

Every response is JSON with a top-level success boolean — check it before touching data.

Success
{
  "success": true,
  "data": { ... }
}
Error
{
  "success": false,
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable message"
  }
}

error.code is a stable machine-readable string (e.g. INSUFFICIENT_BALANCE) — switch on it, not on error.message, which can change. Full list in Errors.

Idempotency

POST /orders is retry-safe. Send an Idempotency-Key header (8–128 chars) — replaying it returns the original order instead of buying twice. Reuse the same key across every retry of one logical purchase (changing max_price on a retry is fine; it isn't part of the key's identity).

A second request with the same key while the first is still being processed gets 409 REQUEST_IN_PROGRESS — retry shortly rather than assuming it failed. Reusing a key with a different catalog_product_id or operator_id gets 422 IDEMPOTENCY_KEY_REUSED; pick a new key for a genuinely different order.

Errors

Failures use HTTP status codes plus the error envelope above. error.code is the stable, machine-readable field — switch on it, not on error.message. error.details additionally carries field-level messages on BAD_REQUEST from a failed body/query check.

CodeHTTPDescription
BAD_REQUEST400Malformed request body/query, or an invalid `Idempotency-Key` header.
INVALID_JSON400Request body is not valid JSON.
UNAUTHORIZED401Missing or invalid Bearer token.
INSUFFICIENT_BALANCE402Wallet balance is below the order or reactivation price.
FORBIDDEN403API key belongs to a suspended account.
NOT_FOUND404No order with that id under your account, or the route doesn’t exist.
CONFLICT409No provider is enabled, the order is already resolved (finished/canceled/expired), already has a code, or the provider can’t resend/reactivate.
CANCEL_TOO_EARLY409The number was bought too recently — the post-purchase hold hasn’t elapsed. `error.details.retryAfterSeconds` says how much longer.
REQUEST_IN_PROGRESS409A create request with this exact `Idempotency-Key` is still being processed — retry shortly.
UNPROCESSABLE422The live price moved above your `max_price`.
NO_OFFER_AVAILABLE422No active offer matches that service/country/tier right now — out of stock, or the tier no longer exists.
IDEMPOTENCY_KEY_REUSED422This `Idempotency-Key` was replayed with a different `catalog_product_id` or `operator_id`. Use a new key for a different logical order.
PROVIDER_ERROR422No number could be rented. `error.details.attempts` has one `{ outcome }` per upstream tried, where `outcome` is `no_numbers`, `provider_unavailable`, or `provider_error`.
RATE_LIMITED429Too many requests — check the `RateLimit-Reset` header (seconds) and back off.
INTERNAL_ERROR500Unexpected server error — safe to retry once.
SERVICE_UNAVAILABLE503Ordering is paused for maintenance.

Rate limits

300 requests per minute per IP. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds until the window resets); once you are over, you get 429 RATE_LIMITED.

Polling an order

  • Poll every 5 seconds. A code rarely arrives sooner, so polling faster only uses up your request budget.
  • Count your parallel orders. 20 orders polled every 5 seconds is 240 requests a minute, close to the limit. If you need more than that, poll less often or contact support.
  • Stop when the order ends. Only waiting needs polling. completed, canceled and expired are final.
  • Set a time limit. Stop at expires_at (about 7 minutes after the purchase). Expired orders are refunded automatically.
  • Back off on a 429. Wait RateLimit-Reset seconds before the next request, then carry on.

The OTP lifecycle

  1. Find a product. GET /catalog/products?service= returns one row per price tier for each country. Each row's id is a stable tier slot. Look it up every time you order. Service codes, country codes and product ids can change as the catalog is updated, so never hardcode them or keep them long-term.
  2. Create the order. POST /orders with catalog_product_id and a max_price cap. The wallet is charged now; the order is waiting with a phone_number.
  3. Poll for the code. GET /orders/:id every 5 seconds until otp_code is set (status becomes completed). Stop early if status turns canceled or expired, or when expires_at passes. If nothing arrives, the order auto-refunds. See Rate limits for polling advice.
  4. Finish (POST /orders/:id/finish) to signal you're done. Or cancel a still-waiting order for a full refund, resend to ask for another SMS, or reactivate a completed order to buy another code on the same number.
End to end
TOKEN="smsg_live_xxxxxxxxxxxx"

# 1. look the product up by name. Never hardcode service codes, country codes or product ids:
#    they can change as the catalog is updated.
SERVICE=$(curl -s "https://api.smsgecko.com/api/v2/catalog/products?search=whatsapp" \
  -H "Authorization: Bearer $TOKEN" \
  | jq -r '[.data[] | select(.service | ascii_downcase == "whatsapp")][0].service_slug')
PID=$(curl -s "https://api.smsgecko.com/api/v2/catalog/products?service=$SERVICE" \
  -H "Authorization: Bearer $TOKEN" \
  | jq -r '[.data[] | select(.country == "United States")][0].id')
[ "$PID" != "null" ] || { echo "no product for that service/country"; exit 1; }

# 2. buy it, never paying more than $1.00 (the order lives at .data on a single-object response)
ORDER=$(curl -s -X POST https://api.smsgecko.com/api/v2/orders/create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d "{\"catalog_product_id\":\"$PID\",\"max_price\":\"1.00\"}")
ID=$(echo "$ORDER" | jq -r '.data.id')
[ "$ID" != "null" ] || { echo "$ORDER"; exit 1; }

# 3. poll every 5 seconds for up to ~7 minutes; stop as soon as the code arrives or the order ends
CODE=""
for i in $(seq 1 84); do
  RES=$(curl -s https://api.smsgecko.com/api/v2/orders/$ID -H "Authorization: Bearer $TOKEN")
  CODE=$(echo "$RES" | jq -r '.data.otp_code // empty')
  STATUS=$(echo "$RES" | jq -r '.data.status')
  [ -n "$CODE" ] && break
  [ "$STATUS" = "waiting" ] || break
  sleep 5
done

# 4. got a code: finish. No code: cancel (full refund; an expired order is refunded for you)
if [ -n "$CODE" ]; then
  echo "code: $CODE"
  curl -s -X POST https://api.smsgecko.com/api/v2/orders/$ID/finish -H "Authorization: Bearer $TOKEN"
else
  curl -s -X POST https://api.smsgecko.com/api/v2/orders/$ID/cancel -H "Authorization: Bearer $TOKEN"
fi

Get wallet balance

GET/api/v2/balance

Your current wallet balance, as the same decimal USD string used everywhere else in the API.

Example request
curl https://api.smsgecko.com/api/v2/balance \
  -H "Authorization: Bearer $TOKEN"
Example response
200 OK
{ "success": true, "data": { "balance": "12.47" } }

List products

GET/api/v2/catalog/products

One product per price tier of a service×country, cheapest first — pass a row's id to POST /orders/create. Without service you instead get a bare service list for discovery (price "0", stock 0, id is just the service code — not orderable). Query with ?service= to get real, buyable rows.

Don't hardcode service codes, country codes or product ids. They can change periodically as providers and the catalog change, so look up the service and country you want by name from this endpoint each time you order (or at least refresh them often). Codes shown in these docs, such as wa or 187, are only examples.

FieldTypeNotes
servicestring?Provider service code (e.g. "wa"). Required for prices.
countrystring?Provider country code (e.g. "187") — narrows the results. A name or ISO code like "us" does not match, and codes can change: read them from the country_code field rather than hardcoding.
searchstring?Without service: filter the service list by name or code (e.g. "whatsapp").
limitint?1–500, default 200.
Example request
curl "https://api.smsgecko.com/api/v2/catalog/products?service=wa&country=187" \
  -H "Authorization: Bearer $TOKEN"
Example response
200 OK
{
  "success": true,
  "data": [
    {
      "id": "wa::187",
      "service": "wa",
      "service_slug": "wa",
      "country": "United States",
      "country_code": "187",
      "operator": null,
      "price": "0.47",
      "stock": 212412
    }
  ]
}

Create an order

POST/api/v2/orders/create

Buys the chosen tier, charges the wallet, rents a number. Returns 201 with a waiting order. POST /api/v2/orders does exactly the same and keeps working, so existing integrations don’t need to change.

FieldTypeNotes
catalog_product_idstringA product `id` from /catalog/products. (`product_id` is accepted too.)
max_pricestring?Decimal USD cap — 422 if the live price exceeds it.
operator_idstring?Pin a carrier: buys the cheapest in-stock tier for it, overriding any tier index in `catalog_product_id`.
Idempotency-Keyheader?8–128 chars — replay-safe create.
Example request
curl -X POST https://api.smsgecko.com/api/v2/orders/create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: 7b1e0c9c-3d21-4b2f-9a10-8f4e2c1d0abc" \
  -H "Content-Type: application/json" \
  -d '{"catalog_product_id":"wa::187","max_price":"0.50"}'
Example response
201 Created
{
  "success": true,
  "data": {
    "id": "SG-8FQ3-K2",
    "status": "waiting",
    "product": { "service": "Whatsapp", "country": "United States" },
    "phone_number": "+15551234567",
    "price": "0.47",
    "otp_code": null,
    "sms": [],
    "created_at": "2026-09-10T14:00:00.000Z",
    "expires_at": "2026-09-10T14:20:00.000Z",
    "finished_at": null
  }
}

Active orders

GET/api/v2/orders/active

Every still-waiting order on your account, newest first: { "success": true, "data": Order[] }.

Example request
curl https://api.smsgecko.com/api/v2/orders/active \
  -H "Authorization: Bearer $TOKEN"
Example response
200 OK
{
  "success": true,
  "data": [
    {
      "id": "SG-8FQ3-K2",
      "status": "waiting",
      "product": { "service": "Whatsapp", "country": "United States" },
      "phone_number": "+15551234567",
      "price": "0.47",
      "otp_code": null,
      "sms": [],
      "created_at": "2026-09-10T14:00:00.000Z",
      "expires_at": "2026-09-10T14:20:00.000Z",
      "finished_at": null
    }
  ]
}

Get an order

GET/api/v2/orders/:id

Poll this for the code. sms[] holds each received message; otp_code is the parsed code once it lands. :id is the order’s id (short, like SG-8FQ3-K2); a legacy 24-hex id still works too.

Example request
curl https://api.smsgecko.com/api/v2/orders/SG-8FQ3-K2 \
  -H "Authorization: Bearer $TOKEN"
Example response
200 OK
{
  "success": true,
  "data": {
    "id": "SG-8FQ3-K2",
    "status": "completed",
    "product": { "service": "Whatsapp", "country": "United States" },
    "phone_number": "+15551234567",
    "price": "0.47",
    "otp_code": "482913",
    "sms": [
      {
        "sender": "WhatsApp",
        "text": "Your WhatsApp code: 482-913",
        "received_at": "2026-09-10T14:03:12.000Z"
      }
    ],
    "created_at": "2026-09-10T14:00:00.000Z",
    "expires_at": "2026-09-10T14:20:00.000Z",
    "finished_at": null
  }
}

Finish an order

POST/api/v2/orders/finish

Marks a completed order finished (finished_at is set) and tells the upstream you're done with the number. No-op if already finished. Send the order id in the JSON body. POST /api/v2/orders/:id/finish does exactly the same and keeps working.

FieldTypeNotes
idstringThe order's `id` (like SG-8FQ3-K2).
Example request
curl -X POST https://api.smsgecko.com/api/v2/orders/finish \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"SG-8FQ3-K2"}'
Example response
200 OK
{
  "success": true,
  "data": {
    "id": "SG-8FQ3-K2",
    "status": "completed",
    "product": { "service": "Whatsapp", "country": "United States" },
    "phone_number": "+15551234567",
    "price": "0.47",
    "otp_code": "482913",
    "sms": [
      {
        "sender": "WhatsApp",
        "text": "Your WhatsApp code: 482-913",
        "received_at": "2026-09-10T14:03:12.000Z"
      }
    ],
    "created_at": "2026-09-10T14:00:00.000Z",
    "expires_at": "2026-09-10T14:20:00.000Z",
    "finished_at": "2026-09-10T14:05:00.000Z"
  }
}

Cancel an order

POST/api/v2/orders/cancel

Cancels a waiting order and refunds it in full. A short post-purchase lock applies — 409 until it elapses, and once a code has arrived.

Send the order id in the JSON body. POST /api/v2/orders/:id/cancel does exactly the same and keeps working.

FieldTypeNotes
idstringThe order's `id` (like SG-8FQ3-K2).
Example request
curl -X POST https://api.smsgecko.com/api/v2/orders/cancel \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"SG-8FQ3-K2"}'
Example response
200 OK
{
  "success": true,
  "data": {
    "id": "SG-8FQ3-K2",
    "status": "canceled",
    "product": { "service": "Whatsapp", "country": "United States" },
    "phone_number": "+15551234567",
    "price": "0.47",
    "otp_code": null,
    "sms": [],
    "created_at": "2026-09-10T14:00:00.000Z",
    "expires_at": "2026-09-10T14:20:00.000Z",
    "finished_at": null
  }
}

sms is always empty here — cancel doesn't look up message history, since a canceled order has none to poll.

Resend a code

POST/api/v2/orders/resend

Asks the upstream for another SMS on a still-waiting order. Free — no new rental, no charge. 409 if the order isn't waiting or the provider can't resend; 422 (PROVIDER_ERROR) if the provider refused or couldn't be reached, so a 200 means another code really was requested. One call every 5 seconds; faster is 429.

Send the order id in the JSON body. POST /api/v2/orders/:id/resend does exactly the same and keeps working.

FieldTypeNotes
idstringThe order's `id` (like SG-8FQ3-K2).
Example request
curl -X POST https://api.smsgecko.com/api/v2/orders/resend \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"SG-8FQ3-K2"}'
Example response
200 OK
{
  "success": true,
  "data": {
    "id": "SG-8FQ3-K2",
    "status": "waiting",
    "product": { "service": "Whatsapp", "country": "United States" },
    "phone_number": "+15551234567",
    "price": "0.47",
    "otp_code": null,
    "sms": [],
    "created_at": "2026-09-10T14:00:00.000Z",
    "expires_at": "2026-09-10T14:20:00.000Z",
    "finished_at": null
  }
}

The response is just the order as it stands right after the request — the resend itself is fire-and-forget upstream. Keep polling GET /orders/:id for the new message.

Reactivate an order

POST/api/v2/orders/reactivate

Buys another code on a completed order's number. Charges the current tier price and reopens the order as waiting, keeping its message history. 402 on low balance; 409 if the order isn't completed or the provider can't reactivate; 422 if that service/country has no offer available right now.

Send the order id in the JSON body. POST /api/v2/orders/:id/reactivate does exactly the same and keeps working.

FieldTypeNotes
idstringThe order's `id` (like SG-8FQ3-K2).
Example request
curl -X POST https://api.smsgecko.com/api/v2/orders/reactivate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"SG-8FQ3-K2"}'
Example response
200 OK
{
  "success": true,
  "data": {
    "id": "SG-8FQ3-K2",
    "status": "waiting",
    "product": { "service": "Whatsapp", "country": "United States" },
    "phone_number": "+15551234567",
    "price": "0.47",
    "otp_code": null,
    "sms": [
      {
        "sender": "WhatsApp",
        "text": "Your WhatsApp code: 482-913",
        "received_at": "2026-09-10T14:03:12.000Z"
      }
    ],
    "created_at": "2026-09-10T14:00:00.000Z",
    "expires_at": "2026-09-10T15:10:00.000Z",
    "finished_at": null
  }
}

id, phone_number and past sms entries carry over from before reactivation; otp_code, status and expires_at reset like a fresh order.

Webhook Notifications

Configure a webhook URL to receive real-time push notifications for order events instead of polling. This is the recommended approach for bot scripts.

Events
EventTrigger
order.otp_receivedAn OTP arrived — same moment `otp_code` is set.
Payload

Every delivery has the same envelope — event, timestamp, and a flat data object (not the same shape as the order object returned by the REST endpoints):

Webhook POST Body
{
  "event": "order.otp_received",
  "timestamp": "2026-09-10T14:03:12.000Z",
  "data": {
    "order_id": "SG-8FQ3-K2",
    "status": "completed",
    "phone_number": "+15551234567",
    "otp_code": "482913",
    "otp_message": "Your WhatsApp code: 482-913",
    "service": "WhatsApp",
    "country": "United States",
    "price": "0.47",
    "created_at": "2026-09-10T14:01:02.000Z",
    "expires_at": "2026-09-10T14:11:02.000Z",
    "finished_at": null
  }
}
FieldTypeNotes
order_idstringShort order id, e.g. "SG-8FQ3-K2".
statusenum"waiting" | "completed" | "canceled" | "expired"
phone_numberstringE.164, e.g. "+15551234567".
otp_codestring | nullParsed code, if one was found in the message.
otp_messagestring | nullRaw text of the SMS that completed the order.
servicestringDisplay name, e.g. "WhatsApp".
countrystringDisplay name, e.g. "United States".
pricestringDecimal USD charged.
created_atstringISO 8601.
expires_atstringISO 8601.
finished_atstring | nullISO 8601, once you call /finish.
Signature verification

Each webhook request includes an X-SMSGecko-Signature: sha256=<hex> header — HMAC-SHA256 of the raw request body using your webhook_secret as the key. Verify this signature on your server before trusting the payload:

import { createHmac, timingSafeEqual } from 'node:crypto';

function isValidSignature(rawBody, header, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}

// rawBody must be the exact bytes received — parse JSON only after verifying.
app.post('/webhooks/smsgecko', express.raw({ type: 'application/json' }), (req, res) => {
  if (!isValidSignature(req.body, req.get('X-SMSGecko-Signature'), process.env.SMSGECKO_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  // ...
  res.sendStatus(200);
});

Delivery is best-effort — one attempt, a 5s timeout, no retry queue. GET /orders/:id always reflects the real state, so treat webhooks as a fast path, not the source of truth.

Get webhook config

GET/api/v2/webhook

Your account's current webhook, or nulls if none is configured yet.

Parameters

None

Example request
curl https://api.smsgecko.com/api/v2/webhook \
  -H "Authorization: Bearer $TOKEN"
Example response
200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhooks/smsgecko",
    "webhook_secret": "3f1c9e7b2a6d4508f0c1e9b7a2d6f435"
  }
}

Set or update the webhook

PATCH/api/v2/webhook

Partial update — send only the fields you're changing.

Body parameters
NameTypeRequiredDescription
webhook_urlstring | nullNoMust be https:// and not a local/private address. Pass null to remove the webhook (and its secret).
webhook_secretstringNo16–128 chars. Omit when first setting webhook_url and one is generated for you; omit on later calls to leave it unchanged.
Example request
curl -X PATCH https://api.smsgecko.com/api/v2/webhook \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://example.com/webhooks/smsgecko"}'
Example response
200 OK
{
  "success": true,
  "data": {
    "webhook_url": "https://example.com/webhooks/smsgecko",
    "webhook_secret": "3f1c9e7b2a6d4508f0c1e9b7a2d6f435"
  }
}

400 BAD_REQUEST if webhook_url isn't https:// or resolves to something like localhost or a private IP range.

Send a test event

POST/api/v2/webhook/test

Fires a synthetic webhook.test event at your configured URL right now, so you can check your receiver end-to-end without waiting for a real order. 409 if no webhook_url is set.

Example request
curl -X POST https://api.smsgecko.com/api/v2/webhook/test \
  -H "Authorization: Bearer $TOKEN"
Example response
200 OK
{
  "success": true,
  "data": { "delivered": true, "statusCode": 200 }
}

delivered reflects whether your endpoint answered with a 2xx — statusCode is null if the request errored or timed out before getting a response at all.

The order object

FieldTypeNotes
idstringShort order id, e.g. "SG-8FQ3-K2" (orders created before this may still be 24-hex).
statusenum"waiting" | "completed" | "canceled" | "expired"
productobject{ service, country } — display names.
phone_numberstringE.164, e.g. "+15551234567".
pricestringDecimal USD charged.
otp_codestring | nullParsed code once delivered.
smsarray[{ sender, text, received_at }]
created_atstringISO 8601.
expires_atstringAuto-refund deadline while waiting.
finished_atstring | nullSet by /finish.

The product object

FieldTypeNotes
idstring"<service>::<country>[::<tier>]" — pass to /orders.
servicestringProvider service code.
service_slugstringSame code (kept for compatibility).
countrystringDisplay name.
country_codestringProvider country code.
operatorstring | nullUpstream operator, when distinguished.
pricestringDecimal USD, markup applied. Variable precision — "0.5", "0.425".
stocknumberReported availability (0 = unknown / none).