fetch.smsAPI v1

SMS verification API reference

Rent non-VOIP US numbers and fetch SMS verification codes programmatically — authentication, endpoints, webhooks, and the live service catalog.

Introduction

The Fetch SMS API lets you rent real US non-VOIP phone numbers and receive SMS verification codes programmatically. It is a JSON REST API over HTTPS.

Conventions
Base URLstringhttps://api.fetchsms.com/v1
MoneyintegerEvery amount is in integer US cents (7000 = $70.00).
TimestampsstringISO-8601 in UTC without an offset suffix, e.g. 2026-06-17T22:14:05.481203 — always read them as UTC.
IDsstringResource ids are UUIDs; services also have a short numeric id.

The fastest path: list services, create a verification, then poll it until the code arrives.

# 1. Pick a service id
curl https://api.fetchsms.com/v1/services

# 2. Rent a number for it
curl https://api.fetchsms.com/v1/verifications \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"service": 4}'

# 3. Poll until "code" is populated (most codes arrive in seconds)
curl https://api.fetchsms.com/v1/verifications/<id> \
  -H "Authorization: Bearer YOUR_API_KEY"

Authentication

Authenticate programmatic verification and rental requests with an API key in the Authorization header as a bearer token. Create and rotate keys on the API page of your dashboard. Keys are shown only once, so store them securely.

Authorization: Bearer YOUR_API_KEY

Dashboard, account, wallet, support, admin, and API-key management routes require a dashboard session token and reject API keys — the one exception is GET /v1/wallet/balance, which accepts an API key so you can check funds before buying. The public catalog endpoints (GET /v1/services and GET /v1/services/quote) do not require a key.

Errors & rate limits

Errors use standard HTTP status codes and a JSON body with a single field:

{ "detail": "Telegram is out of stock for short-term verifications" }
Status codes
200 / 201successRequest succeeded (201 on resource creation).
400bad requestMalformed input or a business-rule violation.
401unauthorizedMissing, invalid, or revoked credential.
402payment requiredInsufficient wallet balance for the purchase.
403forbiddenCredential is valid but not allowed: the endpoint needs a dashboard session, or your daily spend cap is reached.
404not foundUnknown service, or a resource you do not own.
409conflictOut of stock, or the order can no longer be cancelled (a code already arrived, it is not active, or a rental’s 15-minute refund window has passed).
422unprocessableInvalid duration or parameter value.
429rate limitedOver the request rate limit (the response has a Retry-After header), or at an active-number limit: 10 active verifications, and separately 10 active long-term rentals. Let one finish, then retry.

Rate limit: 30 requests/second per API key, with bursts up to 60. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining; a rate-limited 429 adds Retry-After (seconds) — wait that long, then retry. Poll for codes at a sane interval (every 2–5 seconds is plenty).

Services

Fetch SMS has two separate products — short-term and long-term are priced and sold independently:

Products
Short-term verificationper serviceA number for one code, priced per service (WhatsApp, Telegram, …). Each has its own numeric id — see Short-term services.
Long-term rentalsingle productOne flat-priced number that works for any service we support — the "unlimited-services" product (id 1). There is no per-service long-term option.

List the catalog with GET /v1/services. Anywhere the API takes a service, pass the stable numeric id. Slugs like telegram are also accepted for compatibility.

GET/v1/services

Returns an array of services ordered for display. No authentication required.

Response — each item
idintegerStable numeric public id (1…N) — the recommended reference for every endpoint.
slugstringCompatibility reference, e.g. "telegram".
namestringDisplay name, e.g. "Telegram".
price_centsintegerShort-term verification price. Long-term rentals are priced by long_prices.
long_pricesobjectLong-term price per term in cents, e.g. {"1":500,"7":1000,"30":2500,"90":5000,"365":12500} — empty {} except on the Unlimited service.
short_availableintegerNumbers available right now for verifications.
long_availableintegerPristine numbers available right now for Unlimited rentals.
curl https://api.fetchsms.com/v1/services
[
  {
    "id": 2,
    "slug": "whatsapp",
    "name": "WhatsApp",
    "price_cents": 160,
    "long_prices": {},
    "short_available": 200,
    "long_available": 100
  }
]

Long-term rentals are the single Unlimited product, so only it carries a populated long_prices map — every short-term service returns {}. See the full id ↔ service mapping under Short-term services.

GET/v1/services/area-codes

The US area codes available in the pool right now — the options when renting with a custom area code (a surcharge, see Pricing). No authentication required.

Query
modestring"short" (default) or "long". Long-term lists only pristine, never-used numbers.
serviceinteger or stringOptional service reference — returns the codes available for that specific short-term service.
curl "https://api.fetchsms.com/v1/services/area-codes?mode=short&service=2"
["212", "332", "415", "628", "917"]

Pricing & quotes

Short-term verifications are priced per service — each costs that service's verification price (price_cents).

Long-term rentals are a single flat-priced product: one dedicated number that works for any service we support, billed per term. The price does not depend on which services you use the number for.

Long-term rental price · one number, any supported service
1 day$5.00Custom area code +$1.00
7 days$10.00Custom area code +$2.00
30 days$25.00Custom area code +$3.00
90 days$50.00Custom area code +$5.00
365 days$125.00Custom area code +$10.00

A custom area code adds $0.10 to a verification, and to a rental the per-term amount above (surcharge_cents in the quote) — on the rental and again on each extension of it.

GET/v1/services/quote

Compute the exact price before you buy. No authentication required.

Parameters
serviceinteger or string · requiredStable numeric id recommended; slug accepted for compatibility. Use id 1 for long mode.
modestring · required"short" for a verification, "long" for a rental.
daysintegerTerm length for long mode: 1, 7, 30, 90, or 365.
custom_area_codebooleantrue to include the area-code surcharge: $0.10 for a verification, by term for a rental (default false).
curl "https://api.fetchsms.com/v1/services/quote?service=4&mode=short&custom_area_code=true"
{
  "service": "telegram",
  "mode": "short",
  "days": null,
  "base_daily_cents": 70,
  "discount_pct": 0,
  "per_day_cents": null,
  "surcharge_cents": 10,
  "total_cents": 80
}
curl "https://api.fetchsms.com/v1/services/quote?service=1&mode=long&days=30"
{
  "service": "unlimited-services",
  "mode": "long",
  "days": 30,
  "base_daily_cents": 500,
  "discount_pct": 83,
  "per_day_cents": 83,
  "surcharge_cents": 0,
  "total_cents": 2500
}

Verifications (short-term)

A verification reserves a number to receive your verification code inside a fixed 15-minute window. The wallet is debited up front; if the window closes (or you cancel) before any code arrives, the charge is automatically refunded — you are only charged on receipt.

Verifications are per service: the code is released only when the inbound SMS matches the service you purchased — a code for a different service is not shown and the verification keeps waiting. The response and webhook carry the code only, never the message body.

POST/v1/verifications
Body
serviceinteger or string · requiredStable numeric id from GET /v1/services is recommended; slug accepted for compatibility.
area_codestringOptional 3-digit US area code, e.g. "332" (+$0.10).
curl https://api.fetchsms.com/v1/verifications \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"service": 4, "area_code": "332"}'
{
  "id": "9f3e1c2a-…",
  "ref": "v-3f9a1c2e7b",
  "service": "telegram",
  "service_name": "Telegram",
  "number": "+1 (332) 555-0184",
  "code": null,
  "status": "waiting",
  "cost_cents": 75,
  "created_at": "2026-06-17T22:14:05",
  "expires_at": "2026-06-17T22:29:05"
}

status is one of waiting, received, expired, cancelled. Errors: 402 insufficient balance, 404 unknown service, 409 out of stock, 429 you already hold 10 active numbers (waiting, or received and still inside the window) — wait for one to finish or cancel it, then retry. Higher limits are available for volume accounts — contact support.

GET/v1/verifications/{id}

Fetch one verification — poll it until code is non-null and status is received. code is the most recent code received; code_count is how many have arrived in the window so far.

curl https://api.fetchsms.com/v1/verifications/9f3e1c2a-… \
  -H "Authorization: Bearer YOUR_API_KEY"
{ "id": "9f3e1c2a-…", "status": "received", "code": "482910", "code_count": 1, … }
GET/v1/verifications/{id}/messages

Every code this verification has received inside its 15-minute window, newest first. The number stays yours for the whole window, so if more than one code arrives — for example a re-send, or a second login attempt — each is captured here, while code on the verification itself is only the latest. Codes are returned as code + received_at only — the message body is never exposed for short-term verifications.

curl https://api.fetchsms.com/v1/verifications/9f3e1c2a-…/messages \
  -H "Authorization: Bearer YOUR_API_KEY"
[
  {
    "id": "…",
    "code": "552310",
    "received_at": "2026-06-17T22:21:07"
  },
  {
    "id": "…",
    "code": "482910",
    "received_at": "2026-06-17T22:18:44"
  }
]
GET/v1/verifications?tab=active

List your verifications. tab is active (default) or history. On a finished verification that received a code, reusable says whether you can take the same number again right now, reuse_cost_cents what it costs, and reuse_note why not when you can't (see reuse below).

curl "https://api.fetchsms.com/v1/verifications?tab=active" \
  -H "Authorization: Bearer YOUR_API_KEY"
POST/v1/verifications/{id}/cancel

Cancel a waiting verification and refund the charge. Returns 409 once a code has arrived.

curl -X POST https://api.fetchsms.com/v1/verifications/9f3e1c2a-…/cancel \
  -H "Authorization: Bearer YOUR_API_KEY"
POST/v1/verifications/{id}/reuse

Take the number of a finished verification again for a new 15-minute verification of the same service — to receive another code for the account you verified on it. Returns the new verification (201).

When a number can be reused
CoderequiredThe original verification received a code and its window has ended.
Same numberunchangedThe number has not been replaced since. Numbers that have changed can’t be reused.
Freeright nowNo other verification or rental holds the number at the moment.
Yoursfor that serviceYour code was the last one for that service on the number, so no one else can be verified on it there.
PricecurrentThe service’s current price for your account, plus $0.10 if the original used a custom area code. Refunded automatically if no code arrives, like any verification.
curl -X POST https://api.fetchsms.com/v1/verifications/9f3e1c2a-…/reuse \
  -H "Authorization: Bearer YOUR_API_KEY"

Errors: 402 insufficient balance, 404 not your verification, 409 the number can't be reused right now (the detail says why, same as reuse_note), 429 you already hold 10 active numbers. Nothing is reserved or charged when it fails.

Rentals (long-term)

A rental gives you an exclusive US number for 1–365 days that receives unlimited codes from any service we support — one flat-priced product, priced per term (see Pricing), not per service.

How rentals work
Numberfresh, fixedNever used for any service before it is yours. It is yours alone for the whole term and does not change while the rental is active.
Deliverycatalog servicesAn SMS from a service in our catalog is delivered with its full text. Anything else — a bank or other financial app we don’t offer, or a text that matches no service — is withheld: it is not listed in /messages, sets no last_code and sends no webhook.
Refundfirst 15 minutesCancel before cancel_deadline (15 minutes after renting) and before any code arrives for a full refund, extensions included. After that the rental is final — it is never refunded at expiry.
Extendingany termAdd 1, 7, 30, 90 or 365 days to an active rental at that term’s price, as often as you like, up to 730 days ahead. Same number. A rental bought with a custom area code also pays that term’s area-code fee on each extension.
Limit10 activeUp to 10 active rentals per account, counted separately from your 10 active verifications.
The rental object
idstringRental id (UUID).
refstringShort reference, e.g. "r-7fa322a308".
servicestringAlways "unlimited-services".
service_namestringAlways "Unlimited Services".
numberstringYour number, e.g. "+1 (415) 555-0142".
duration_daysintegerTotal days: the term you rented plus every extension.
extended_daysintegerDays added by extensions (0 if none).
custom_area_codebooleanRented with a chosen area code — extensions then add that term’s area-code fee.
statusstring"waiting" (no code yet), "received" (at least one code delivered), "expired" or "cancelled". A received rental stays active until it expires.
last_codestring | nullThe most recent code delivered.
cost_centsintegerTotal charged: the rental plus every extension.
refundedbooleanWhether the charge was refunded (only a cancel inside the window refunds).
created_atstringWhen you rented it (UTC).
expires_atstringWhen it ends (UTC); extensions move it.
cancel_deadlinestringLast moment a cancel is refunded: created_at + 15 minutes (UTC).
POST/v1/rentals

Rent a number. The full price is charged from your wallet at once.

Body
serviceinteger or string · requiredThe long-term product: id 1 (slug "unlimited-services" also accepted).
daysinteger · requiredTerm: 1, 7, 30, 90 or 365.
area_codestringOptional 3-digit US area code (+$1.00 to +$10.00 by term, see Pricing). Available codes: GET /v1/services/area-codes?mode=long.
curl https://api.fetchsms.com/v1/rentals \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"service": 1, "days": 30}'
{
    "id": "1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d",
    "ref": "r-7fa322a308",
    "service": "unlimited-services",
    "service_name": "Unlimited Services",
    "number": "+1 (415) 555-0142",
    "duration_days": 30,
    "extended_days": 0,
    "custom_area_code": false,
    "last_code": null,
    "status": "waiting",
    "cost_cents": 2500,
    "refunded": false,
    "created_at": "2026-06-17T22:14:05",
    "expires_at": "2026-07-17T22:14:05",
    "cancel_deadline": "2026-06-17T22:29:05"
  }
Errors
402payment requiredInsufficient wallet balance — nothing is charged.
403forbiddenYour daily spend cap is reached.
404not foundUnknown service, or long-term rentals are paused.
409conflictOut of stock (or no number in the requested area code).
422unprocessableTerm is not 1, 7, 30, 90 or 365 days, or the service is not the long-term product.
429too manyYou already hold 10 active rentals — wait for one to end, or extend one instead.
GET/v1/rentals/{id}

Fetch one of your rentals — poll it (every 2–5 seconds) or use webhooks to watch for codes. last_code is the most recent code delivered. 404 if it is not yours.

curl https://api.fetchsms.com/v1/rentals/1a2b3c4d-… \
    -H "Authorization: Bearer YOUR_API_KEY"
GET/v1/rentals/{id}/messages

Every SMS delivered to the rental, newest first. Withheld messages are not listed.

Response — each item
idstringMessage id.
senderstringSender as reported by the carrier.
bodystringThe full message text.
codestringThe verification code extracted from it.
received_atstringWhen it arrived (UTC).
curl https://api.fetchsms.com/v1/rentals/1a2b3c4d-…/messages \
    -H "Authorization: Bearer YOUR_API_KEY"
[
    {
      "id": "5a112326-1e5e-4409-979f-9559238a1fd3",
      "sender": "WhatsApp",
      "body": "Your WhatsApp code: 821-193\nDon't share this code with others",
      "code": "821-193",
      "received_at": "2026-06-17T22:31:10"
    }
  ]
GET/v1/rentals

List your rentals, newest first.

Query
tabstring"active" (default): waiting or received and not yet past expires_at. "history": expired or cancelled.
limitinteger1–200, default 100.
curl "https://api.fetchsms.com/v1/rentals?tab=active" \
    -H "Authorization: Bearer YOUR_API_KEY"
POST/v1/rentals/{id}/extend

Add a term to an active rental. The days go on top of the current expires_at, at the same price as renting that term, charged at once. The number stays the same. If the rental was bought with a custom area code (custom_area_code true), that term’s area-code fee is added too ($1.00 to $10.00, see Pricing). Repeat as often as you like, up to 730 days ahead. An extension is not refundable on its own — it is refunded only with the whole rental, by a cancel before cancel_deadline.

Body
daysinteger · requiredTerm to add: 1, 7, 30, 90 or 365.
curl https://api.fetchsms.com/v1/rentals/1a2b3c4d-…/extend \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"days": 30}'

Returns the updated rental. Extending the 30-day rental above by 30 days moves expires_at by 30 days and gives duration_days 60, extended_days 30 and cost_cents 5000 (both totals; 5600 had it been rented with a custom area code).

Errors
402payment requiredInsufficient wallet balance — nothing changes.
403forbiddenYour daily spend cap is reached.
404not foundNot your rental, or long-term rentals are paused.
409conflictThe rental is not active (expired or cancelled).
422unprocessableTerm is not 1, 7, 30, 90 or 365 days, or it would run more than 730 days ahead.
POST/v1/rentals/{id}/cancel

Cancel and refund in full — the rental and any extensions — only before cancel_deadline (15 minutes after renting) and only if no code has been delivered. The number is released. Returns the rental with status "cancelled" and refunded true.

curl -X POST https://api.fetchsms.com/v1/rentals/1a2b3c4d-…/cancel \
    -H "Authorization: Bearer YOUR_API_KEY"
Errors
404not foundNot your rental.
409conflictA code was already delivered, the rental is not active, or the 15-minute window has passed.

Wallet

Purchases are paid from your prepaid wallet. Check your balance before buying to avoid a 402. Deposits and ledger history are dashboard-only.

GET/v1/wallet/balance

Your spendable balance. Reads a single row, so it costs the same whether you have ten transactions or a hundred thousand — safe to call before every purchase.

Response
balance_centsintegerSpendable balance in cents. 0 if you have never funded the account.
curl https://api.fetchsms.com/v1/wallet/balance \
  -H "Authorization: Bearer YOUR_API_KEY"

{ "balance_cents": 12345 }

Amounts are always integer cents. A purchase that costs more than your balance returns 402 with a detail message, and nothing is charged.

Wholesale

A wholesale batch is a block of numbers we set up for your account for a fixed term, held for the services you agreed with us (or every service). While it runs, every code those services send to its numbers is yours, and the numbers don't change. Messages from any other app aren't shown. Batches are arranged with us; these endpoints read them.

GET/v1/wholesale

Your batches, newest first.

[
  {
    "id": "6c1e…",
    "ref": "w-3f9a1c2e7b",
    "unlimited": false,
    "services": [{ "slug": "seated", "name": "Seated" }],
    "lines": 1000,
    "hours": 48,
    "status": "active",
    "starts_at": "2026-10-06T18:00:00",
    "ends_at": "2026-10-08T18:00:00",
    "codes": 412
  }
]
GET/v1/wholesale/{id}/numbers

The batch's numbers, each with its code count and latest code. search matches digits; limit (up to 1000) and offset page through them. The full list is also at /v1/wholesale/{id}/numbers.csv.

GET/v1/wholesale/{id}/messages

Codes delivered on the batch, newest first, with the full message. To poll, pass the last received_at you saw as since (UTC) and get only newer ones.

curl "https://api.fetchsms.com/v1/wholesale/6c1e…/messages?since=2026-10-06T18:42:10" \
  -H "Authorization: Bearer YOUR_API_KEY"

{
  "items": [
    {
      "id": "…",
      "number": "+1 (332) 555-0184",
      "service": "seated",
      "service_name": "Seated",
      "code": "7613",
      "body": "7613 is your verification code for Seated.",
      "received_at": "2026-10-06T18:43:02"
    }
  ],
  "total": 1
}

Each code also fires sms.received to your webhooks, with wholesale_batch_id, the batch ref, the service slug and the full body.

GET/v1/wholesale/lines

Every number of your batches, one row each, the way the dashboard lists them with your long-term rentals. tab=active (default) while the batch runs, tab=history once it has ended. status is waiting or received while active, ended after.

[
  {
    "id": "a41d…",
    "batch_id": "6c1e…",
    "ref": "w-3f9a1c2e7b",
    "service_name": "Seated",
    "number": "+1 (332) 555-0184",
    "hours": 48,
    "last_code": "7613",
    "codes": 1,
    "status": "received",
    "created_at": "2026-10-06T18:00:00",
    "expires_at": "2026-10-08T18:00:00"
  }
]
GET/v1/wholesale/lines/{id}/messages

The codes one number received, newest first: id, sender, body, code and received_at, the same shape as a rental's messages.

Webhooks

Instead of polling, register an endpoint to receive events the moment they happen. Add and manage endpoints on the API page. Each delivery is a POST with a JSON body.

An account can register up to 10 endpoints. The URL must be a public http or https address with no username or password in it; addresses on private, loopback or cloud-metadata networks are refused, both when you save the endpoint and at every delivery.

Events
sms.receivedeventA code arrived on a verification, an SMS from a supported service on a rental, or a code on a wholesale batch (withheld messages send nothing).
rental.expiredeventA long-term rental reached the end of its term.

Delivery headers:

Parameters
X-FetchSMS-EventstringThe event type, e.g. "sms.received".
X-FetchSMS-SignaturestringHMAC-SHA256 hex of the raw body, keyed by your endpoint secret.

Example sms.received payload:

{
  "event": "sms.received",
  "data": {
    "verification_id": "9f3e1c2a-…",
    "ref": "v-3f9a1c2e7b",
    "number": "+1 (332) 555-0184",
    "sender": "Telegram",
    "code": "482910",
    "detected_service": "Telegram",
    "received_at": "2026-06-17T22:15:30"
  },
  "created_at": "2026-06-17T22:15:30"
}

Short-term verification events carry the code only — never the message body. A long-term rental event carries rental_id (instead of verification_id) and includes the full body:

{
  "event": "sms.received",
  "data": {
    "rental_id": "1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d",
    "ref": "r-7fa322a308",
    "number": "+1 (415) 555-0142",
    "sender": "WhatsApp",
    "code": "821-193",
    "detected_service": "WhatsApp",
    "received_at": "2026-06-17T22:31:10",
    "body": "Your WhatsApp code: 821-193\nDon't share this code with others"
  },
  "created_at": "2026-06-17T22:31:10"
}

rental.expired fires when a rental reaches its expires_at (extensions move it):

{
  "event": "rental.expired",
  "data": {
    "rental_id": "1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d",
    "ref": "r-7fa322a308",
    "number": "+1 (415) 555-0142"
  },
  "created_at": "2026-07-17T22:14:31"
}

Verify the signature before trusting a delivery:

import crypto from 'node:crypto'

function verify(rawBody, signature, endpointSecret) {
  const expected = crypto
    .createHmac('sha256', endpointSecret)
    .update(rawBody)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}

Short-term services

Every short-term verification service with its stable numeric id, slug, and price — pass the id to POST /v1/verifications. Live per-service stock is returned by GET /v1/services (the short_available field). The long-term unlimited-services rental product is not a short-term service and is excluded here (see Pricing).

Short-term verification services