Skip to content

API reference

Programmatic access to your NumberHub wallet, numbers, rentals, eSIMs, and email OTPs.

v1REST · JSONBearer authUSD wallet

Developer resources

Getting started

Base URL

All endpoints are rooted here; every response is JSON.

Authentication

Send your secret as a Bearer token on every request — the header only (a key in the URL would leak into logs). A key acts as your account and spends your wallet; keep it private. Manage keys in Account.

Quick start
Check your balance
curl https://api.numberhub.io/v1/balance \
  -H "Authorization: Bearer nh_live_..."
Money model

SMS & Email are charge-on-receive: buying only holdsthe price; you’re billed once a code arrives — no code, no charge. Rent & eSIM are charged upfront. All amounts are USD; available = balance − held.

Versioning and asynchronous delivery

The major version is part of the base URL. Backward-compatible fields and endpoints may be added to v1; removals or incompatible semantic changes require a new major version and a migration notice in the changelog.

A unique Idempotency-Key is required on every purchase request. Keep the same key when retrying a timeout. The first result is retained for 24 hours and safely replayed for identical retries; reusing the key with a different route or body returns 409. This protects numbers, rentals, eSIMs, and email orders from duplicate charges after a timeout.

For push delivery, create a webhook and verify X-NumberHub-Signature as HMAC-SHA256 over timestamp.raw_body, using the exact raw request body and the X-NumberHub-Timestamp value. Compare the result as v1=<hex digest> and reject stale timestamps. Events contain the order id and status, not OTP contents; fetch the order with your API key after notification. Delivery is at least once, so deduplicate using the event delivery id. Failed attempts retry for seven days, with backoff capped at 24 hours. Use the delivery-history endpoint to diagnose failures and rotate a signing secret immediately if it may have leaked.

Objects

The order object

Every order response — SMS numbers, rentals, eSIMs, and emails — returns this same object (wrapped as number / rental / esim / email). The per-endpoint examples below highlight the fields that change for that call.

idintegerOrder id.
kindstring"sms" · "rent" · "esim" · "email".
servicestringService code (SMS/rent) or package code (eSIM).
service_namestringHuman name of the service / package.
countrystringCountry id / region code.
country_namestringHuman country / region name.
phonestring | nullNumber (SMS/rent), ICCID (eSIM), or inbox address (email).
statusstringpending · waiting · received · completed · canceled · expired.
status_labelstringHuman-readable status.
pricestringAmount held/charged, e.g. "0.43" — pair with currency USD.
codestring | nullLatest received code (null until one arrives).
codesarrayEvery code received so far.
created_atstringISO-8601 timestamp.
expires_atstringISO-8601 expiry.
esim { ready, qr, ac, smdp, matchingId, iccid, short_url }
On /esims responses — the installable profile (QR + manual SM-DP+/activation).
email { address, code }
On /emails responses — the mailbox address and the received OTP.

Account

Wallet
GET/balance

Wallet balance for the key’s owner. available = balance − held; held is reserved by open orders.

Request
curl https://api.numberhub.io/v1/balance \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "balance": "12.40",
  "available": "9.15",
  "held": "3.25",
  "currency": "USD"
}
GET/orders?limit=40

Your most recent orders across all products (sms, rent, esim, email). limit is 1–100 (default 40).

Request
curl "https://api.numberhub.io/v1/orders?limit=40" \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "orders": [
    { "id": 10482, "kind": "sms", "service": "tg",
      "country": "0", "status": "received",
      "price": "0.43", "code": "1234", "codes": ["1234"] }
  ]
}

SMS numbers

One-time codes
GET/services

SMS services you can buy a number for. code is what you pass to /numbers and /countries.

Request
curl https://api.numberhub.io/v1/services \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "services": [
    { "code": "tg", "name": "Messaging app" },
    { "code": "wa", "name": "Messaging app" },
    { "code": "go", "name": "Email provider" }
  ]
}
GET/countries?service=tg

Countries for a service with live sell price + stock. Out-of-stock entries queue when bought. Unknown service → 404.

Request
curl "https://api.numberhub.io/v1/countries?service=tg" \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "service": "tg",
  "countries": [
    { "country": "0", "name": "Russia", "flag": "RU",
      "price": "0.43", "in_stock": true },
    { "country": "187", "name": "United States", "flag": "US",
      "price": "1.10", "in_stock": false }
  ]
}
GET/top-countries

Smart Top 10 using 30-day demand, live stock, starting price, and delivery quality (refreshed ~10 min). Raw order volumes are private.

Request
curl https://api.numberhub.io/v1/top-countries \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "countries": [
    { "country": "151", "name": "Chile", "iso": "CL",
      "from_price": "0.14", "in_stock_services": 8,
      "delivery_rate": 96, "delivery_sample": 184 }
  ]
}
POST/numbers

Buy a number. The price is HELD, not charged — you’re billed only when a code arrives. Returns 201. Optional max_price (USD) is a safety cap — if the number’s price is above it, nothing is bought and you get 409 price_exceeded with the actual price. Optional queue (boolean, default true): false = synchronous request/response mode — one sourcing attempt, and if nothing is available right now you get 409 sold_out immediately with nothing created or held (retry whenever you like); true = out-of-stock buys become a background search that keeps hunting for up to 4 h. Up to 50 concurrent open orders per service+country (409 duplicate_order beyond).

Body: service, country, max_price?, queue?

Request
curl -X POST https://api.numberhub.io/v1/numbers \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: order-550e8400-e29b-41d4-a716" \
  -H "Content-Type: application/json" \
  -d '{"service":"tg","country":"0","max_price":"1.00"}'
Response
{
  "number": {
    "id": 10482, "kind": "sms",
    "service": "tg", "service_name": "Messaging app",
    "country": "0", "country_name": "Russia",
    "phone": "79991234567",
    "status": "waiting", "status_label": "Waiting for code",
    "price": "0.43", "code": null, "codes": [],
    "created_at": "2026-06-16T12:00:00+00:00",
    "expires_at": "2026-06-16T12:20:00+00:00"
  }
}
GET/numbers/{id}

Poll one order. When a code lands, status → "received" and code / codes fill in (and you’re charged once).

Request
curl https://api.numberhub.io/v1/numbers/10482 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "number": {
    "id": 10482, "status": "received",
    "status_label": "Code received",
    "phone": "79991234567", "price": "0.43",
    "code": "1234", "codes": ["1234"]
  }
}
DELETE/numbers/{id}

Cancel + release the hold (no charge). Activation cancels are only accepted ~2 min after issue (else 409 cancel_locked).

Request
curl -X DELETE https://api.numberhub.io/v1/numbers/10482 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "ok": true,
  "number": { "id": 10482, "status": "canceled",
    "status_label": "Cancelled (not charged)" }
}
POST/numbers/{id}/another

Request another code on the same number. Places a fresh hold; 402 if available balance is too low.

Request
curl -X POST https://api.numberhub.io/v1/numbers/10482/another \
  -H "Authorization: Bearer nh_live_..."
Response
{ "ok": true, "number": { "id": 10482, "status": "waiting" } }
POST/numbers/{id}/reactivate

Reuse a completed/expired number to receive a fresh code (billable, same charge-on-receive flow).

Request
curl -X POST https://api.numberhub.io/v1/numbers/10482/reactivate \
  -H "Authorization: Bearer nh_live_..."
Response
{ "ok": true, "number": { "id": 10482, "status": "waiting" } }

Rent

Long-lived numbers
GET/rent/durations

Available rental durations. h (hours) is the value used in the routes below.

Request
curl https://api.numberhub.io/v1/rent/durations \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "durations": [
    { "h": 4, "label": "4 hours" },
    { "h": 24, "label": "1 day" },
    { "h": 168, "label": "1 week" }
  ]
}
GET/rent/{h}/countries

Countries that have rentals in stock for a duration. Unknown h → 404.

Request
curl https://api.numberhub.io/v1/rent/24/countries \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "h": 24,
  "countries": [ { "country": "0", "name": "Russia", "flag": "RU" } ]
}
GET/rent/{h}/{country}/services

Rentable services + price for a duration + country.

Request
curl https://api.numberhub.io/v1/rent/24/0/services \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "h": 24, "country": "0",
  "services": [ { "code": "tg", "name": "Messaging app", "price": "3.20" } ]
}
GET/rent/{h}/{country}/{code}/quote

Exact price + affordability before buying a rental.

Request
curl https://api.numberhub.io/v1/rent/24/0/tg/quote \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "h": 24, "country": "0", "code": "tg",
  "service_name": "Messaging app", "country_name": "Russia", "flag": "RU",
  "duration_label": "1 day", "price": "3.20",
  "available": "9.15", "can_afford": true
}
POST/rentals

Rent a number for the whole period (charged UPFRONT). Returns 201 with the rental order.

Body: h, country, code

Request
curl -X POST https://api.numberhub.io/v1/rentals \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: rent-550e8400-e29b-41d4-a716" \
  -H "Content-Type: application/json" \
  -d '{"h":24,"country":"0","code":"tg"}'
Response
{
  "rental": {
    "id": 10590, "kind": "rent", "service": "tg",
    "country": "0", "phone": "79990001122",
    "status": "waiting", "price": "3.20"
  }
}
GET/rentals/{id}

Poll a rental for the codes it has received (a rental collects many codes for the period).

Request
curl https://api.numberhub.io/v1/rentals/10590 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "rental": { "id": 10590, "status": "received",
    "phone": "79990001122", "codes": ["1234", "5678"] }
}
DELETE/rentals/{id}

Cancel + refund — only inside the 2–20 min window (else 409 cancel_window_closed).

Request
curl -X DELETE https://api.numberhub.io/v1/rentals/10590 \
  -H "Authorization: Bearer nh_live_..."
Response
{ "ok": true, "rental": { "id": 10590, "status": "canceled" } }
POST/rentals/{id}/finish

End a rental early without a refund (frees the number).

Request
curl -X POST https://api.numberhub.io/v1/rentals/10590/finish \
  -H "Authorization: Bearer nh_live_..."
Response
{ "ok": true, "rental": { "id": 10590, "status": "completed" } }

eSIM

Travel data
GET/esim/regions

Destinations (countries + regional bundles) you can buy a data eSIM for.

Request
curl https://api.numberhub.io/v1/esim/regions \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "regions": [
    { "code": "US", "name": "United States", "flag": "US" },
    { "code": "EU", "name": "Europe", "flag": "EU" }
  ]
}
GET/esim/regions/{region}/packages

Data plans for a destination — pkg.code is what you pass to POST /esims.

Request
curl https://api.numberhub.io/v1/esim/regions/US/packages \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "region": "US", "region_name": "United States",
  "packages": [
    { "code": "US_1GB_7D", "name": "USA 1GB 7Days",
      "gb": "1 GB", "duration": 7, "unit": "day",
      "price": "3.50", "scope": "Local",
      "is_local": true, "countries": 1 }
  ]
}
POST/esims

Buy an eSIM (charged upfront). The QR profile is usually ready within seconds — poll GET /esims/{id}.

Body: region, pkg

Request
curl -X POST https://api.numberhub.io/v1/esims \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: esim-550e8400-e29b-41d4-a716" \
  -H "Content-Type: application/json" \
  -d '{"region":"US","pkg":"US_1GB_7D"}'
Response
{
  "esim": {
    "id": 10610, "kind": "esim", "status": "waiting",
    "price": "3.50",
    "esim": { "ready": false }
  }
}
GET/esims/{id}

Poll for the installable profile: QR image, the LPA activation string, and the manual SM-DP+ / activation code.

Request
curl https://api.numberhub.io/v1/esims/10610 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "esim": {
    "id": 10610, "status": "received",
    "esim": {
      "ready": true,
      "qr": "https://.../qr.png",
      "ac": "LPA:1$rsp.example.com$ABC123",
      "smdp": "rsp.example.com",
      "matchingId": "ABC123",
      "iccid": "8943...",
      "short_url": "https://.../p/abc"
    }
  }
}

Email OTP

Disposable inbox
GET/email/sites

Sites you can receive a sign-up OTP email for.

Request
curl https://api.numberhub.io/v1/email/sites \
  -H "Authorization: Bearer nh_live_..."
Response
{ "sites": [ { "code": "shop", "name": "Shopping site" } ] }
GET/email/sites/{site}/domains

Available inbox domains + price for a site. domain is optional on buy (cheapest in stock is used).

Request
curl https://api.numberhub.io/v1/email/sites/shop/domains \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "site": "shop",
  "site_name": "Shopping site",
  "domains": [ { "domain": "inboxes.com", "price": "0.20", "in_stock": true } ]
}
POST/emails

Buy a disposable inbox (charge-on-receive). Returns 201 with the order + mailbox address.

Body: site, domain?

Request
curl -X POST https://api.numberhub.io/v1/emails \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: email-550e8400-e29b-41d4-a716" \
  -H "Content-Type: application/json" \
  -d '{"site":"amazon"}'
Response
{
  "email": {
    "id": 10700, "kind": "email", "status": "waiting",
    "price": "0.20",
    "email": { "address": "[email protected]", "code": null }
  }
}
GET/emails/{id}

Poll the inbox. When the OTP email lands, code fills in and you’re charged once.

Request
curl https://api.numberhub.io/v1/emails/10700 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "email": {
    "id": 10700, "status": "received",
    "email": { "address": "[email protected]", "code": "549182" }
  }
}

Webhooks

Signed events
GET/webhooks

List active webhook endpoints and delivery health. Signing secrets are never returned after creation.

Request
curl https://api.numberhub.io/v1/webhooks \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "webhooks": [
    { "id": 12, "url": "https://example.com/numberhub-events",
      "events": ["order.*"], "active": true,
      "failure_count": 0, "last_success_at": null,
      "secret_hint": "whsec_abc…" }
  ]
}
POST/webhooks

Register a public HTTPS endpoint. events defaults to ["order.*"]. The whsec_ signing secret is returned once; save it securely. Up to five active endpoints per account.

Body: url, events?

Request
curl -X POST https://api.numberhub.io/v1/webhooks \
  -H "Authorization: Bearer nh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/numberhub-events","events":["order.*"]}'
Response
{
  "webhook": {
    "id": 12, "url": "https://example.com/numberhub-events",
    "events": ["order.*"],
    "secret": "whsec_save_this_once"
  }
}
POST/webhooks/{id}/test

Queue a webhook.test event so you can validate networking and signature verification.

Request
curl -X POST https://api.numberhub.io/v1/webhooks/12/test \
  -H "Authorization: Bearer nh_live_..."
Response
{ "queued": true }
POST/webhooks/{id}/rotate

Replace the signing secret immediately. The new secret is returned once; update your receiver before sending more events.

Request
curl -X POST https://api.numberhub.io/v1/webhooks/12/rotate \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "webhook": { "id": 12, "url": "https://example.com/numberhub-events" },
  "secret": "whsec_save_this_once"
}
GET/webhooks/{id}/deliveries

Inspect recent attempts, delivery state, and the last sanitized error for one endpoint.

Request
curl https://api.numberhub.io/v1/webhooks/12/deliveries \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "deliveries": [
    { "id": "c17f4c58f0c54d77910d4ef0e93363f7", "event": "order.received", "order_id": 10700,
      "attempts": 1, "pending": false, "delivered_at": "2026-08-01T10:30:00Z",
      "last_error": null }
  ]
}
DELETE/webhooks/{id}

Deactivate an endpoint immediately. New deliveries will no longer be queued.

Request
curl -X DELETE https://api.numberhub.io/v1/webhooks/12 \
  -H "Authorization: Bearer nh_live_..."
Response
{ "ok": true }

Rate limits & errors

Rate limits

60 requests / 10s per key (sliding window — bursts to 60, ~6 req/s sustained), and 180 requests / 10s aggregated per account across all your keys. Sustained use at the limit is fine indefinitely; need more for a high-volume integration? Contact us via the bot. Over the limit returns 429 with a Retry-After header — back off and retry.

{ "error": "rate_limited" }
Error shape

Errors use the HTTP status plus a stable machine code:

{ "error": "insufficient_funds" }
401
invalid_api_key
Missing/unknown/blocked key, or a key passed in the query string (header only).
429
rate_limited
Over the limit — back off (see Retry-After).
400
missing_params / missing_service / bad_request
A required field was empty or the body wasn’t a JSON object.
404
unknown_service / not_found
No such service, or no such order/resource under this key.
400
invalid_duration
Rent {h} isn’t one of the offered durations.
409
sold_out
No stock for that combination right now.
409
duplicate_order
You already have the maximum open orders for this service+country (50 via the API).
400
idempotency_key_required
Every purchase requires an Idempotency-Key header.
400
invalid_idempotency_key
Idempotency-Key must be 8–128 visible ASCII characters.
409
idempotency_conflict / idempotency_in_progress
The key was reused for another request, or its first request is still running.
409
cancel_locked / cancel_window_closed
Outside the allowed cancel/refund window.
402
insufficient_funds
Available balance can’t cover the hold.
503
esim_disabled / email_disabled
That product is temporarily unavailable.
502
purchase_failed
Upstream provider error — safe to retry.
Ready to build?
Mint a key and make your first call in under a minute.
Create API key