Skip to content
New: Reseller bot. Sell numbers in your own Telegram bot, under your name.

API reference

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

  • v1
  • REST · JSON
  • Bearer auth
  • USD wallet

Developer resources

Getting started

Base URL

All endpoints are rooted here. Responses are JSON except proxy setup-file downloads.

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.

Browser access (CORS)

Cross-origin requests from third-party websites are not supported. Send API requests from your server. Never put a live API key in client-side code.

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. SMS is billed when the first code arrives. Email is billed when the verification email arrives, whether it contains a code or a link. If no SMS code or verification email arrives for that order, its hold is released. 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.

Paid endpoints accept an optional max_price in the JSON body, as a positive USD decimal string such as "3.20". This caps the order or top-up price, not the per-GB rate. For POST /numbers/bulk, the cap applies per number, not to the batch total. A quote does not lock the price. If a bulk purchase stops on price, a refusal can return 409 without price fields, or a partial batch can return 201 with purchased numbers and stopped="price". Check the returned numbers before requesting another batch. For a single-order request, a higher price returns 409 price_exceeded; an increase beyond the reserved API allowance returns 409 price_changed. Check a fresh quote and confirm the new amount before making another purchase request. After a definite price rejection, use a new idempotency key for the newly confirmed request where required.

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. SMS reactivation also supports this replay middleware when an optional Idempotency-Key is supplied. Keep the same key, method, path and exact JSON bytes for uncertain retries. Without a key, fetch the SMS order before deciding whether to repeat reactivation. Free SMS another-code and email reorder actions do not use this middleware. After an uncertain response for those actions, fetch the SMS order or list recent email orders before deciding whether to repeat.

SMS billing example: a $0.20 order reserves $0.20 and charges it when its first code arrives. That is the only charge for the number. A duplicate notification does not create another paid request, and “Request another code” is free: nothing is held or charged, and the next code is added to the same order when it arrives. 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

SMS numbers, rentals, eSIMs, emails and proxies share these order fields (wrapped as number / rental / esim / email / proxy). GET /orders returns these common fields without the product subobjects below. Dedicated product endpoints add their respective subobject. The examples show selected fields.

idintegerOrder id.
kindstring"sms" · "rent" · "esim" · "email" · "proxy".
servicestringService or package identifier.
service_namestringHuman name of the service / package.
countrystringProduct-specific country, region, or domain identifier.
country_namestringHuman country, region, or domain name.
phonestring | nullNumber (SMS/rent), ICCID (eSIM), inbox address (email), or null.
verificationstring"sms" or "call". The delivery mode for SMS activations; defaults to "sms" for other products.
statusstringpending · waiting · received · completed · canceled · expired.
status_labelstringHuman-readable status.
pricestringUSD decimal amount held or charged, e.g. "0.43", without a currency symbol.
codestring | nullLatest parsed code, or null. Link-only emails, eSIMs and proxies can have no code.
codesarrayReceived code strings. Empty for eSIMs and proxies.
operatorstring | nullSMS carrier, or null while unassigned and for other products.
created_atstring | nullISO-8601 creation timestamp when recorded.
expires_atstring | nullISO-8601 expiry when set.
cancel_available_atstring | nullEarliest cancellation time for a waiting SMS activation; otherwise null.
esim { ready, qr, ac, smdp, matchingId, iccid, short_url, install }
On /esims responses. install is an object with ios and android install URLs when an activation string is available, or an empty object. Other profile values can be null while delivery is pending.
email { address, code, link, links, message }
On /emails responses. code and link can be null; links is an array and message is plain text (up to 4000 characters) or null. A received verification email may contain a link without a parsed code. code also covers codes written in groups ("475-256") or alone on their own line; take it from code, not from message.
proxy { host, http_port, socks_port, username, password, gb, validity_days, country, lines, telegram_url, setup, usage }
On /proxies responses. Credentials and package values can be null; lines and setup are empty objects until credentials are available. usage is null unless usage figures are included. Keep credentials private.

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

POST/topups

Top up the wallet without leaving your system: open a USDT deposit and send pay_amount USDT to address, on that network only, before expires_at. The balance is credited by itself a few minutes after the network confirms; paying exactly pay_amount credits exactly amount (USD, 1.00 to 10000.00). method is usdt_bsc (BNB Smart Chain, BEP20) or usdt_tron (Tron, TRC20). Asking again for the same amount and network returns the open deposit (200, reused=true) instead of a new one, so a retry is safe without an Idempotency-Key. Needs orders:write. At most 10 open deposits per account (429 too_many_open_topups). Returns 201.

Body: amount, method

Request
curl -X POST https://api.numberhub.io/v1/topups \
  -H "Authorization: Bearer nh_live_..." \
  -H "Content-Type: application/json" \
  -d '{"amount":"50.00","method":"usdt_tron"}'
Response
{
  "topup": {
    "id": 4812, "status": "pending",
    "amount": "50.00", "credited": null, "currency": "USD",
    "method": "usdt_tron", "network_short": "TRC20",
    "network": "Tron (TRC20)", "pay_currency": "USDT",
    "address": "TExampleAddressOnlyForDocs00000000",
    "pay_amount": "51.00",
    "expires_at": "2026-10-22T10:00:00Z", "reused": false
  }
}

GET/topups/{id}

A deposit and its status: pending, paid (credited is what was added) or expired. Answered from our records, so it is cheap to poll every 30 to 60 seconds. address and pay_amount turn null once the deposit can no longer be paid: money sent to an expired address is lost. Needs wallet:read.

Request
curl https://api.numberhub.io/v1/topups/4812 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "topup": {
    "id": 4812, "status": "paid",
    "amount": "50.00", "credited": "50.00", "currency": "USD",
    "method": "usdt_tron", "network_short": "TRC20",
    "address": null, "pay_amount": null,
    "paid_at": "2026-10-20T10:12:31Z"
  }
}

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, prices, and stock for a service. The catalog is cached and may use the last successful list during a temporary outage. Stock is checked again when placing an order. 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, catalog stock, starting price, and delivery quality (refreshed ~10 min). During a temporary outage, catalog data may come from the last successful update. 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 filter by the latest available stock data for that service (counts are not exposed). Carriers remain listed when their stock cannot be checked. An empty list means the carrier is assigned automatically 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 attempt to get a number, 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). 409 purchase_failed with reason low_delivery means new numbers are paused for your account because of bulk activity that isn’t getting codes (numbers taken and cancelled in bulk with almost no codes, or a burst of buy attempts that found no number): nothing is held, and it lifts by itself as that activity rolls out of the last 24 hours (or the last hour, for buy attempts). 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. Free: a number is billed once, so nothing is held or charged and the balance is not needed. The number stays "received"; poll it, and the next code is added to codes when it arrives. A number that cannot receive another code answers 409 another_unsupported.

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

POST/numbers/{id}/reactivate

Reuse an eligible number to receive a fresh code (billable, same charge-on-receive flow). An optional Idempotency-Key enables response replay. On an uncertain retry, reuse the same key, method, path and exact JSON bytes. Without a key, fetch the order before deciding whether to repeat the action.

Body: max_price?

Request
curl -X POST https://api.numberhub.io/v1/numbers/10482/reactivate \
  -H "Authorization: Bearer nh_live_..." \
  -H "Idempotency-Key: reuse-550e8400-e29b-41d4-a716"
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

Rental countries for a duration. During a temporary outage, this may show the last successful country list. Availability is checked again when buying. 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

Rental services and prices for a duration and country. During a temporary outage, this may show the last successful price list.

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

Price and affordability from the latest available rental catalog. During a temporary outage, the quote may use cached prices. Send its price as max_price when buying to cap your spend; a quote does not reserve stock or lock the price.

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. Purchases always use fresh prices; when they cannot be checked, the request returns 503 rent_unavailable. Set max_price to your accepted quote price: a higher refreshed price returns 409 price_exceeded before buying.

Body: h, country, code, max_price?

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","max_price":"3.20"}'
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 is issued once the cancellation is confirmed 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, max_price?

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",
      "install": {
        "ios": "https://esimsetup.apple.com/esim_qrcode_provisioning?carddata=LPA:1$rsp.example.com$ABC123",
        "android": "https://esimsetup.android.com/esim_qrcode_provisioning?carddata=LPA:1$rsp.example.com$ABC123"
      }
    }
  }
}

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, country?, max_price?

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 | sticky. Sticky sessions request the same IP for a period, subject to availability; a new session id does not guarantee a different IP. count = 1-100 lines with distinct session ids in sticky mode; ttl = 60s…24h, default 24h, is the requested sticky session duration; 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, max_price?

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": [ { "site": "amazon.com", "name": "Amazon" } ] }

GET/email/sites/{site}/domains

Available inbox domains + price for a site. domain is optional on buy (cheapest in stock is used). Examples are illustrative; fetch domains for current stock and prices.

Request
curl https://api.numberhub.io/v1/email/sites/amazon.com/domains \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "site": "amazon.com",
  "site_name": "Amazon",
  "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?, max_price?

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.com"}'
Response
{
  "email": {
    "id": 10700, "kind": "email", "status": "waiting",
    "price": "0.20",
    "email": { "address": "x7gq@inboxes.com", "code": null,
      "link": null, "links": [], "message": null }
  }
}

GET/emails/{id}

Poll the inbox. You’re charged once when the verification email arrives. An order delivers one mail; a mail with no code and no verify or sign-in link (a “new device” notice, a welcome mail) is skipped: the order stays waiting on the same address, expires_at moves 20 minutes on, and you’re charged for the next mail (at most 2 skips per order). It may contain a code or a link; code can remain null when the email has no parsed code. Read the code from email.code and the link from email.link: message is the mail’s own text, and its spacing and line breaks differ between senders and inbox domains, so don’t parse it. On the website, an email order’s page shows this exact response under “View raw (API response)”.

Request
curl https://api.numberhub.io/v1/emails/10700 \
  -H "Authorization: Bearer nh_live_..."
Response
{
  "email": {
    "id": 10700, "status": "received",
    "email": {
      "address": "x7gq@inboxes.com", "code": null,
      "link": "https://example.com/verify?token=example",
      "links": ["https://example.com/verify?token=example"],
      "message": "Verify your email: https://example.com/verify?token=example"
    }
  }
}

POST/emails/{id}/reorder

Order the same mailbox again as a new order after it 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. 409 purchase_failed includes a message when the mailbox cannot be reordered right now.

Body: max_price?

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": "x7gq@inboxes.com", "code": null,
      "link": null, "links": [], "message": 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
bad_max_price
max_price must be a positive, finite USD amount.
409
price_exceeded
The price exceeds max_price. For a single-order request, no purchase is made and the response includes the current price. Bulk refusals can omit price fields; a partial batch returns 201 with its purchased numbers and stopped="price".
409
price_changed
The price increased beyond the reserved API allowance during purchase. No purchase is made; the response includes the current price.
409
daily_spend_limit_exceeded
The order would exceed this API key's daily spending limit.
400
idempotency_key_required
New orders and proxy top-ups require 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. The message says how to top up: POST /topups, or the wallet page.
400
invalid_amount / amount_out_of_range / unknown_method
POST /topups: amount must be 1.00 to 10000.00 USD; method must be usdt_bsc or usdt_tron.
429
too_many_open_topups
Ten deposits are already open. Pay or let one expire first; asking again for an open amount and network still returns it.
502
topup_failed
A deposit address could not be opened right now. No deposit was opened; retry in a minute.
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 could not be issued). 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