Skip to content
New: Residential proxies — your own login in 150+ countries, paid per GB from the same wallet.

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 holds the price; you’re billed when the requested code arrives — no code, no charge. Rentals, eSIMs & proxies 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 for new number, rental, eSIM, email and proxy orders, and for proxy data top-ups. Keep the same key and exact JSON bytes when retrying a timeout within 24 hours. A completed result is retained for 24 hours and replayed for identical retries; reusing the key with a different route or body returns 409. Another-code and reactivation actions do not use this middleware: fetch the order after an uncertain response before deciding whether to repeat the action.

SMS billing example: a $0.20 order reserves $0.20 and charges it when its first code arrives. A duplicate notification does not create another paid request. Selecting “Request another code” reserves a fresh $0.20; that hold is charged only when the new code arrives, or released if it does not. Reactivation starts a new paid activation window at the applicable price; it is not a free duplicate SMS. Delivery alone does not guarantee the target platform accepts a code or account.

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, proxy). 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 }
  ]
}

GET/countries/{country}/operators

Carriers you can ask for by name in that country. Add ?service=tg to keep only carriers that have stock for that service right now (counts are not exposed). An empty list means the provider picks the carrier and POST /numbers must be sent without "operator". GET /countries rows carry the unfiltered list as "operators".

Request
curl "https://api.numberhub.io/v1/countries/175/operators?service=tg"   -H "Authorization: Bearer nh_live_..."
Response
{ "country": "175", "operators": ["lebara", "optus", "telstra", "vodafone"] }

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. Optional verification: "sms" (default) or "call" — a voice call reads the code out instead of an SMS; list the voice pool first with /countries?service=…&verification=call, since it is smaller and priced separately. Optional operator: a carrier name from GET /countries/{country}/operators, or several separated by commas ("telstra,optus") — the number is sourced only from those carriers; an unknown name is 400 bad_operator with the accepted list, and a country with no carrier choice is 400 operators_unavailable. Up to 50 concurrent open orders per service+country (409 duplicate_order beyond). The response echoes verification and carries operator (the carrier that issued the number, null while queued).

Body: service, country, max_price?, queue?, verification?, operator?

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", "verification": "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": [],
    "operator": "any",
    "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}

Read rental status and received codes. Use GET /orders and filter kind=rent locally for recent rental history. The public v1 API does not expose rental extension; use the available extension controls in the web app or Telegram.

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}

Request cancellation 2 to 20 minutes after the rental starts, only while no SMS has arrived. A refund requires provider confirmation and returns to the NumberHub balance. Outside the window: 409 cancel_window_closed. A refused cancellation returns ok=false and leaves the rental active. There is no early termination after this window.

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

Retired. Rentals run for their full period and complete automatically; ending early gave no refund, so this now answers 409 finish_disabled and leaves the rental untouched.

Request
curl -X POST https://api.numberhub.io/v1/rentals/10590/finish \
  -H "Authorization: Bearer nh_live_..."
Response
{ "error": "finish_disabled", "message": "Rentals run for their full period and end automatically; ending early is no longer offered." }

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"
    }
  }
}

Proxies

Residential data packages

GET/proxy/packages

Residential proxy packages with live retail prices and validity. save_percent is the saving against the same package before its volume discount, rounded down to a whole percentage; it is not a discount on the markup. from_per_gb is the cheapest per-GB price. enabled=false with an empty list means the product is unavailable. These examples are illustrative, not current quotes.

Request
curl https://api.numberhub.io/v1/proxy/packages \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "enabled": true,
  "validity_days": 30,
  "currency": "USD",
  "from_per_gb": "0.84",
  "packages": [
    {
      "code": "1gb",
      "gb": 1,
      "price": "0.98",
      "per_gb": "0.98",
      "save_percent": 0,
      "popular": false
    },
    {
      "code": "5gb",
      "gb": 5,
      "price": "4.65",
      "per_gb": "0.93",
      "save_percent": 5,
      "popular": true
    },
    {
      "code": "20gb",
      "gb": 20,
      "price": "16.66",
      "per_gb": "0.84",
      "save_percent": 15,
      "popular": false
    }
  ]
}

POST/proxies

Buy a data package, paid upfront from the wallet. A temporary hold protects the balance while the purchase is processed; a successful purchase charges it and returns credentials with the package traffic cap. Returns 201. proxy.lines contains ready-made host:port:login:password strings for rotating, country-targeted and sticky sessions over HTTP or SOCKS5. Access stops when data is exhausted or validity ends. 402 insufficient_funds; 409 purchase_failed when the pool is unavailable, with no charge. Prices below are illustrative; use /proxy/packages for current quotes.

Body: gb

Request
curl -X POST https://api.numberhub.io/v1/proxies \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: proxy-550e8400-e29b-41d4-a716" \
  -H "Content-Type: application/json" \
  -d '{"gb": 5}'
Response
{
  "proxy": {
    "id": 10820, "kind": "proxy", "status": "received",
    "service_name": "5 GB residential proxy", "price": "4.65",
    "expires_at": "2026-07-16T12:00:00+00:00",
    "proxy": {
      "host": "proxy.numberhub.io", "http_port": 8080, "socks_port": 1080,
      "username": "nh10820_k3x9qa", "password": "Zq8…",
      "gb": 5, "validity_days": 30,
      "lines": {
        "any": "proxy.numberhub.io:8080:nh10820_k3x9qa:Zq8…",
        "country": "proxy.numberhub.io:8080:nh10820_k3x9qa-country-us:Zq8…",
        "sticky": "proxy.numberhub.io:8080:nh10820_k3x9qa-country-us-sid-nh1a2b3c:Zq8…",
        "socks": "proxy.numberhub.io:1080:nh10820_k3x9qa-country-us:Zq8…"
      },
      "usage": null
    }
  }
}

GET/proxies/{id}

Read one proxy order (credentials + lines). Same shape as the purchase response.

Request
curl https://api.numberhub.io/v1/proxies/10820 \
  -H "Authorization: Bearer nh_live_..."
Response
{ "proxy": { "id": 10820, "status": "received", "proxy": { "username": "nh10820_k3x9qa", "gb": 5 } } }

GET/proxies/{id}/usage

Live traffic figures metered by the gateway: bytes plus human-readable GB and the percentage left.

Request
curl https://api.numberhub.io/v1/proxies/10820/usage \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "proxy": {
    "id": 10820,
    "proxy": {
      "usage": { "used_bytes": 1610612736, "limit_bytes": 5368709120,
                 "left_bytes": 3758096384, "used": "1.5", "total": "5",
                 "left": "3.5", "left_pct": 70 }
    }
  }
}

GET/proxies/{id}/stats?period=7d

Traffic over time for the login: period = today | hours24 | 7d | 30d. bytes and gb are per bucket (hour or day), with a total.

Request
curl "https://api.numberhub.io/v1/proxies/10820/stats?period=7d" \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "id": 10820,
  "stats": { "period": "7d", "labels": ["29 Jun", "30 Jun"],
             "bytes": [734003200, 1073741824], "gb": ["0.68", "1"],
             "total_bytes": 1807745024, "total": "1.68" }
}

GET/proxy/countries?type=residential

Targeting catalog: every country the pool covers (ISO-2 code, name). /proxy/countries/{cc}/cities lists the exact city ids the pool accepts for that country (pass the id to /proxies/{id}/lines); regions are free-form — lower-case with underscores for spaces (california, salto_department). /proxy/countries/{cc}/regions exists for compatibility and returns an empty list.

Request
curl https://api.numberhub.io/v1/proxy/countries \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "type": "residential",
  "countries": [
    { "code": "us", "name": "United States", "availability": "" },
    { "code": "mx", "name": "Mexico", "availability": "" }
  ]
}

POST/proxies/{id}/lines

Build ready-made host:port:login:password lines for a package. country = ISO-2 (omit for worldwide rotation); region/city narrow it (city ids from /proxy/countries/{cc}/cities; lower-case, underscores for spaces: california, new_york); session = rotating (new IP per request) | sticky (same IP; count = 1–100 lines with distinct session ids, ttl = 60s…24h, default 24h); protocol = http | socks5. Nothing is stored — call it as often as you like. 400 invalid_options names the bad field.

Body: country?, region?, city?, session?, sid?, ttl?, protocol?, count?

Request
curl -X POST https://api.numberhub.io/v1/proxies/10820/lines \
  -H "Authorization: Bearer nh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"country":"mx","session":"sticky","ttl":"1h","protocol":"socks5","count":3}'
Response
{
  "id": 10820,
  "options": { "country": "mx", "region": null, "city": null, "session": "sticky", "sid": null, "ttl": "1h", "protocol": "socks5", "count": 3 },
  "lines": [
    "proxy.numberhub.io:1080:nh10820_k3x9qa-country-mx-sid-nh7a2c19-ttl-1h:Zq8…",
    "proxy.numberhub.io:1080:nh10820_k3x9qa-country-mx-sid-nh0f44be-ttl-1h:Zq8…",
    "proxy.numberhub.io:1080:nh10820_k3x9qa-country-mx-sid-nh91d3aa-ttl-1h:Zq8…"
  ]
}

POST/proxies/{id}/country

Set the package’s default country (ISO-2). The ready-made lines in every later response target it.

Body: country

Request
curl -X POST https://api.numberhub.io/v1/proxies/10820/country \
  -H "Authorization: Bearer nh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"country":"mx"}'
Response
{ "proxy": { "id": 10820, "proxy": { "country": "mx", "lines": { "country": "proxy.numberhub.io:8080:nh10820_k3x9qa-country-mx:Zq8…" } } } }

GET/proxies/{id}/setup/{kind}

Download a ready-made setup file for the package: kind = clash (Clash YAML for Clash Verge, Clash Meta and Stash: rotating HTTP, country-pinned sticky HTTP and SOCKS5 proxies), windows (a .bat that toggles the Windows system proxy) or macos (a .command that sets the proxy with the login). The proxy object also carries telegram_url, Telegram’s native one-tap proxy link, and setup with these three paths.

Request
curl -OJ https://api.numberhub.io/v1/proxies/10820/setup/clash \
  -H "Authorization: Bearer nh_live_..."
Response
# numberhub-proxy-10820.yaml (Content-Disposition: attachment)
proxies:
  - { name: "NumberHub rotating", type: http, server: "proxy.numberhub.io", port: 8080, username: "nh10820_k3x9qa", password: "Zq8…" }
  - { name: "NumberHub US sticky", type: http, server: "proxy.numberhub.io", port: 8080, username: "nh10820_k3x9qa-country-us-sid-cl3f9a1b", password: "Zq8…" }
  - { name: "NumberHub SOCKS5", type: socks5, server: "proxy.numberhub.io", port: 1080, username: "nh10820_k3x9qa", password: "Zq8…" }

POST/proxies/{id}/topup

Add data to an ACTIVE package: same login, bigger cap, validity restarted at the full window from today. Paid upfront from the wallet (fixed price from /proxy/packages). Returns 201 with the refreshed package and the ledger entry. 402 insufficient_funds; 409 purchase_failed when the pool is out of stock (nothing charged).

Body: gb

Request
curl -X POST https://api.numberhub.io/v1/proxies/10820/topup \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: topup-550e8400-e29b-41d4-a716" \
  -H "Content-Type: application/json" \
  -d '{"gb": 5}'
Response
{
  "proxy": { "id": 10820, "service_name": "10 GB residential proxy",
             "expires_at": "2026-08-15T12:00:00+00:00", "proxy": { "gb": 10 } },
  "topup": { "id": 10901, "kind": "proxy", "status": "completed", "price": "4.65" }
}

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" }
  }
}

POST/emails/{id}/reorder

Order the SAME mailbox again as a new order — on an inbox that received a mail (received or completed), even days later. Charge-on-receive like a fresh buy. 409 not_reorderable while the inbox is waiting, cancelled or expired (a mailbox that never got its first mail cannot be reordered); 502 purchase_failed with a message if the provider refuses.

Request
curl -X POST https://api.numberhub.io/v1/emails/10700/reorder \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "email": {
    "id": 10812, "kind": "email", "status": "waiting",
    "price": "0.20",
    "email": { "address": "[email protected]", "code": null }
  }
}

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.
409
purchase_failed
The purchase could not be completed (for example the item is out of stock or the upstream refused). Nothing was charged — safe to retry later.
Ready to build?
Mint a key and make your first call in under a minute.
Create API key