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.
| Base URL | string | https://api.fetchsms.com/v1 |
| Money | integer | Every amount is in integer US cents (7000 = $70.00). |
| Timestamps | string | ISO-8601 in UTC without an offset suffix, e.g. 2026-06-17T22:14:05.481203 — always read them as UTC. |
| IDs | string | Resource 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_KEYDashboard, 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" }| 200 / 201 | success | Request succeeded (201 on resource creation). |
| 400 | bad request | Malformed input or a business-rule violation. |
| 401 | unauthorized | Missing, invalid, or revoked credential. |
| 402 | payment required | Insufficient wallet balance for the purchase. |
| 403 | forbidden | Credential is valid but not allowed: the endpoint needs a dashboard session, or your daily spend cap is reached. |
| 404 | not found | Unknown service, or a resource you do not own. |
| 409 | conflict | Out 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). |
| 422 | unprocessable | Invalid duration or parameter value. |
| 429 | rate limited | Over 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:
| Short-term verification | per service | A number for one code, priced per service (WhatsApp, Telegram, …). Each has its own numeric id — see Short-term services. |
| Long-term rental | single product | One 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.
/v1/servicesReturns an array of services ordered for display. No authentication required.
| id | integer | Stable numeric public id (1…N) — the recommended reference for every endpoint. |
| slug | string | Compatibility reference, e.g. "telegram". |
| name | string | Display name, e.g. "Telegram". |
| price_cents | integer | Short-term verification price. Long-term rentals are priced by long_prices. |
| long_prices | object | Long-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_available | integer | Numbers available right now for verifications. |
| long_available | integer | Pristine 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.
/v1/services/area-codesThe 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.
| mode | string | "short" (default) or "long". Long-term lists only pristine, never-used numbers. |
| service | integer or string | Optional 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.
| 1 day | $5.00 | Custom area code +$1.00 |
| 7 days | $10.00 | Custom area code +$2.00 |
| 30 days | $25.00 | Custom area code +$3.00 |
| 90 days | $50.00 | Custom area code +$5.00 |
| 365 days | $125.00 | Custom 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.
/v1/services/quoteCompute the exact price before you buy. No authentication required.
| service | integer or string · required | Stable numeric id recommended; slug accepted for compatibility. Use id 1 for long mode. |
| mode | string · required | "short" for a verification, "long" for a rental. |
| days | integer | Term length for long mode: 1, 7, 30, 90, or 365. |
| custom_area_code | boolean | true 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.
/v1/verifications| service | integer or string · required | Stable numeric id from GET /v1/services is recommended; slug accepted for compatibility. |
| area_code | string | Optional 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.
/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, … }/v1/verifications/{id}/messagesEvery 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"
}
]/v1/verifications?tab=activeList 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"/v1/verifications/{id}/cancelCancel 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"/v1/verifications/{id}/reuseTake 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).
| Code | required | The original verification received a code and its window has ended. |
| Same number | unchanged | The number has not been replaced since. Numbers that have changed can’t be reused. |
| Free | right now | No other verification or rental holds the number at the moment. |
| Yours | for that service | Your code was the last one for that service on the number, so no one else can be verified on it there. |
| Price | current | The 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.
| Number | fresh, fixed | Never 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. |
| Delivery | catalog services | An 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. |
| Refund | first 15 minutes | Cancel 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. |
| Extending | any term | Add 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. |
| Limit | 10 active | Up to 10 active rentals per account, counted separately from your 10 active verifications. |
| id | string | Rental id (UUID). |
| ref | string | Short reference, e.g. "r-7fa322a308". |
| service | string | Always "unlimited-services". |
| service_name | string | Always "Unlimited Services". |
| number | string | Your number, e.g. "+1 (415) 555-0142". |
| duration_days | integer | Total days: the term you rented plus every extension. |
| extended_days | integer | Days added by extensions (0 if none). |
| custom_area_code | boolean | Rented with a chosen area code — extensions then add that term’s area-code fee. |
| status | string | "waiting" (no code yet), "received" (at least one code delivered), "expired" or "cancelled". A received rental stays active until it expires. |
| last_code | string | null | The most recent code delivered. |
| cost_cents | integer | Total charged: the rental plus every extension. |
| refunded | boolean | Whether the charge was refunded (only a cancel inside the window refunds). |
| created_at | string | When you rented it (UTC). |
| expires_at | string | When it ends (UTC); extensions move it. |
| cancel_deadline | string | Last moment a cancel is refunded: created_at + 15 minutes (UTC). |
/v1/rentalsRent a number. The full price is charged from your wallet at once.
| service | integer or string · required | The long-term product: id 1 (slug "unlimited-services" also accepted). |
| days | integer · required | Term: 1, 7, 30, 90 or 365. |
| area_code | string | Optional 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"
}| 402 | payment required | Insufficient wallet balance — nothing is charged. |
| 403 | forbidden | Your daily spend cap is reached. |
| 404 | not found | Unknown service, or long-term rentals are paused. |
| 409 | conflict | Out of stock (or no number in the requested area code). |
| 422 | unprocessable | Term is not 1, 7, 30, 90 or 365 days, or the service is not the long-term product. |
| 429 | too many | You already hold 10 active rentals — wait for one to end, or extend one instead. |
/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"/v1/rentals/{id}/messagesEvery SMS delivered to the rental, newest first. Withheld messages are not listed.
| id | string | Message id. |
| sender | string | Sender as reported by the carrier. |
| body | string | The full message text. |
| code | string | The verification code extracted from it. |
| received_at | string | When 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"
}
]/v1/rentalsList your rentals, newest first.
| tab | string | "active" (default): waiting or received and not yet past expires_at. "history": expired or cancelled. |
| limit | integer | 1–200, default 100. |
curl "https://api.fetchsms.com/v1/rentals?tab=active" \
-H "Authorization: Bearer YOUR_API_KEY"/v1/rentals/{id}/extendAdd 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.
| days | integer · required | Term 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).
| 402 | payment required | Insufficient wallet balance — nothing changes. |
| 403 | forbidden | Your daily spend cap is reached. |
| 404 | not found | Not your rental, or long-term rentals are paused. |
| 409 | conflict | The rental is not active (expired or cancelled). |
| 422 | unprocessable | Term is not 1, 7, 30, 90 or 365 days, or it would run more than 730 days ahead. |
/v1/rentals/{id}/cancelCancel 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"| 404 | not found | Not your rental. |
| 409 | conflict | A 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.
/v1/wallet/balanceYour 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.
| balance_cents | integer | Spendable 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.
/v1/wholesaleYour 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
}
]/v1/wholesale/{id}/numbersThe 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.
/v1/wholesale/{id}/messagesCodes 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.
/v1/wholesale/linesEvery 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"
}
]/v1/wholesale/lines/{id}/messagesThe 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.
| sms.received | event | A 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.expired | event | A long-term rental reached the end of its term. |
Delivery headers:
| X-FetchSMS-Event | string | The event type, e.g. "sms.received". |
| X-FetchSMS-Signature | string | HMAC-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).
