Receive SMS online

by Unknown

Not rated
Website

About

Buy a number for a single SMS verification, rent one for months, and get pushed a webhook the moment a code arrives. Everything the website does, over HTTPS.

Details

Author
Unknown
Categories
Marketing, Other, Communication

Buy a number for a single SMS verification, rent one for months, and get pushed a webhook the moment a code arrives. Everything the website does, over HTTPS.

The SMSZ API gives you programmatic access to everything the website does: buy a number for a single SMS verification, rent a number for days or months, read incoming messages, and get pushed a webhook the moment a code arrives.

Everything is JSON over HTTPS. All amounts are inUSD, all timestamps areISO 8601 in UTC, and every purchase is charged against your account balance — top it up from the website before your first call.

The current API version is2026-07-01, returned on every response as theSMSZ-Versionheader. Additive changes (new fields, new endpoints, new event types) ship without a version bump, sowrite clients that ignore unknown fields.

Every request carries an API key as a bearer token:

Authorization: Bearer smsz_live_9f2a1c4d_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Scopes.Each key carries an explicit list of permissions. Grant only what an integration needs: a server that just buys numbers has no reason to holdwebhooks:write.

- account:read
- activations:read
- activations:write
- rentals:read
- rentals:write
- webhooks:read
- webhooks:write

A call missing the scope it needs fails withinsufficient_scopeand names the missing scope.

IP allowlist.A key can be pinned to one or more IPv4 addresses or CIDR ranges. Requests from anywhere else are refused withip_not_allowed. Worth doing for keys that live on a fixed server.

Keeping keys safe.Keys are bearer credentials — anyone holding one can spend your balance. Keep them server-side. Never ship one in a browser bundle, a mobile app, or a public repository. If a key leaks, revoke it in the dashboard; revocation takes effect immediately.

curl https://www.smsz.net/api/v1/ping \ -H "Authorization: Bearer $SMSZ_API_KEY"

Buying a number and reading its code takes two calls.

curl -X POST https://www.smsz.net/api/v1/activations \ -H "Authorization: Bearer $SMSZ_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"country": "US", "service": "telegram"}'
{ "id": "cmg7x2k9a0001l208hq3v7bqz", "object": "activation", "status": "pending", "phone_number": "+12025550147", "price": 0.62, "expires_at": "2026-07-24T10:30:03.000Z", "messages": [] }

Usephone_numberwherever you are signing up. Your balance is debited immediately; if the provider cannot fill the order you are not charged at all.

curl https://www.smsz.net/api/v1/activations/cmg7x2k9a0001l208hq3v7bqz/messages \ -H "Authorization: Bearer $SMSZ_API_KEY"
{ "object": "list", "data": [ { "id": "cmg7xb1s70006l208r9y2mnop", "object": "message", "sender": "Telegram", "text": "Telegram code 51284", "code": "51284", "received_at": "2026-07-24T10:16:44.000Z" } ], "has_more": false, "total": 1 }

Poll every 3-5 seconds untildatais non-empty, oruse a webhookand skip polling entirely.codeis the digits we extracted;textis the full message if the extraction misses.

curl -X POST https://www.smsz.net/api/v1/activations/cmg7x2k9a0001l208hq3v7bqz/finish \ -H "Authorization: Bearer $SMSZ_API_KEY"

Not required, but it releases the number early. If no code ever arrived, finishing refunds you — as does letting the activation expire on its own.

const API = "https://www.smsz.net/api/v1"; const headers = { Authorization: \Bearer ${process.env.SMSZ_API_KEY}\, "Content-Type": "application/json", }; async function getVerificationCode(country, service) { const created = await fetch(\${API}/activations\, { method: "POST", headers: { ...headers, "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ country, service }), }); if (!created.ok) { const { error } = await created.json(); throw new Error(\${error.code}: ${error.message}\); } const activation = await created.json(); console.log("Use this number:", activation.phone_number); const deadline = Date.parse(activation.expires_at); while (Date.now() < deadline) { await new Promise((r) => setTimeout(r, 4000)); const res = await fetch(\${API}/activations/${activation.id}/messages\, { headers }); const { data } = await res.json(); if (data.length > 0) return data[0].code; } throw new Error("No SMS arrived before the activation expired"); }
import os, time, uuid, requests API = "https://www.smsz.net/api/v1" session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['SMSZ_API_KEY']}" def get_verification_code(country: str, service: str) -> str: response = session.post( f"{API}/activations", json={"country": country, "service": service}, headers={"Idempotency-Key": str(uuid.uuid4())}, ) if not response.ok: error = response.json()["error"] raise RuntimeError(f"{error['code']}: {error['message']}") activation = response.json() print("Use this number:", activation["phone_number"]) for _ in range(60): time.sleep(4) messages = session.get(f"{API}/activations/{activation['id']}/messages").json() if messages["data"]: return messages["data"][0]["code"] raise TimeoutError("No SMS arrived before the activation expired")

An activation is a number rented for a single verification. It lives for roughly 15-20 minutes depending on the provider, andexpires_attells you exactly when.

Choosing what to buy.countryaccepts an ISO code, our slug, or the country name —US,united-statesandUnited Statesall work.serviceis a slug fromGET /services. Check price and stock first withGET /pricing/activations?country=US&service=telegram.

Omitoperatorand we pick the cheapest one with stock. Omitprovidertoo — pinning a provider only narrows what we can fill the order from.

Refunds are automatic.You are never charged for an activation that received nothing. If the window closes empty, the balance goes back on its own. Cancelling early does the same thing sooner. Once a message arrives the activation has done its job and is not refundable.

Errors worth handling.insufficient_balance(402) means top up.number_unavailable(409) means that country and service pair has no stock right now — try another country, or checkGET /pricing/activationsfor what is actually available.

A rental holds a number for days or months and receives unlimited messages for the period.

Ordering.Rentals are bought against an offer fromGET /pricing/rentals, because availability and price vary by country and length:

curl "https://www.smsz.net/api/v1/pricing/rentals?country=GB&days=30" \ -H "Authorization: Bearer $SMSZ_API_KEY"
{ "object": "list", "data": [ { "offer_id": "o1.Xn9pQ2s.7Kd1fA.k3mZq0vR8tYw", "country": "GB", "country_name": "United Kingdom", "duration_days": 30, "price": 14.5, "currency": "USD", "available": 62 } ] }

Pass that row'soffer_idstraight back — it already fixes the country, the length and the price:

curl -X POST https://www.smsz.net/api/v1/rentals \ -H "Authorization: Bearer $SMSZ_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"offer_id": "o1.Xn9pQ2s.7Kd1fA.k3mZq0vR8tYw"}'

offer_idis opaque and short-lived: treat it as a token to round-trip, not a value to parse or store. It expires after30 minutes, so fetch offers immediately before ordering rather than caching them. An expired or altered token is rejected withinvalid_parameter— fetch a fresh one and retry.

Per-service rentals.Addingservicerents one service on the number rather than the whole number. Cheaper when you only need one platform.

Extending.POST /rentals/{id}/extendwith{"days": 30}adds time and charges your balance. Extend beforeexpires_at— an expired rental cannot be revived, only replaced.

Cancelling.Rentals are refundablewithin 120 minutes of purchase and only if no message has been received— that is the window our providers give us, so it is the window we can offer. Outside it the call returnsnot_cancellable.

Webhooks push events to your server as they happen, so you do not have to poll. This is the recommended way to consume the API.

curl -X POST https://www.smsz.net/api/v1/webhooks/endpoints \ -H "Authorization: Bearer $SMSZ_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/smsz", "events": ["activation.message.received", "rental.message.received"] }'

The response contains asecretstartingwhsec_.This is the only time it is returned.Store it — it is what proves a delivery came from us.

{ "id": "cmg7xh2k4000cl208a1b2c3d4", "object": "event", "type": "activation.message.received", "api_version": "2026-07-01", "created": 1784889404, "data": { "id": "cmg7x2k9a0001l208hq3v7bqz", "object": "activation", "status": "completed", "phone_number": "+12025550147", "messages": [ { "id": "cmg7xb1s70006l208r9y2mnop", "object": "message", "sender": "Telegram", "text": "Telegram code 51284", "code": "51284", "received_at": "2026-07-24T10:16:44.000Z" } ] } }

datais the same object the REST endpoints return, so one deserializer handles both.

Every delivery carries aSMSZ-Signatureheader:

SMSZ-Signature: t=1784889404,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1is HMAC-SHA256 of{timestamp}.{raw request body}, keyed with your endpoint secret. Verify it onthe raw body, before any JSON parsing— re-serializing changes the bytes and the signature will not match.

import crypto from "node:crypto"; function verifySmszWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) { const parts = Object.fromEntries( signatureHeader.split(",").map((p) => p.split("=").map((s) => s.trim())) ); // Reject old deliveries so a captured payload cannot be replayed later. if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false; const expected = crypto .createHmac("sha256", secret) .update(\${parts.t}.${rawBody}\) .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)); } // Express — note express.raw(), not express.json() app.post("/hooks/smsz", express.raw({ type: "application/json" }), (req, res) => { if (!verifySmszWebhook(req.body.toString(), req.get("SMSZ-Signature"), process.env.SMSZ_WEBHOOK_SECRET)) { return res.status(400).send("bad signature"); } const event = JSON.parse(req.body.toString()); // Acknowledge first, work afterwards: we retry anything that is not a 2xx. res.status(200).send("ok"); handleEvent(event).catch(console.error); });
import hmac, hashlib, time from flask import Flask, request, abort def verify_smsz_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool: parts = dict(p.strip().split("=", 1) for p in signature_header.split(",")) # Reject old deliveries so a captured payload cannot be replayed later. if abs(time.time() - int(parts["t"])) > tolerance: return False expected = hmac.new( secret.encode(), f"{parts['t']}.{raw_body.decode()}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, parts["v1"]) @app.post("/hooks/smsz") def smsz_webhook(): if not verify_smsz_webhook(request.get_data(), request.headers["SMSZ-Signature"], SECRET): abort(400) event = request.get_json() enqueue(event) # acknowledge fast, process out of band return "", 200

- Any2xxis an acknowledgement. Anything else is retried.
- Retries:8 attemptsat 10s → 30s → 2m → 10m → 30m → 2h → 6h → 12h — just over 24 hours in total.
- Deliveries time out after10 seconds, so acknowledge immediately and do the work asynchronously.
- Redirects arenotfollowed. If your URL moves, update the endpoint.
- After20 consecutive failuresan endpoint is disabled. Re-enable it withPATCH /webhooks/endpoints/{id}and{"status": "active"}.
- Delivery isat-least-onceand ordering isnot guaranteed. Deduplicate on the eventid, and prefer the state indataover inferring it from event sequence.

Debugging.POST /webhooks/endpoints/{id}/testsends a real signed event with obviously fake data and reports what your server answered.GET /webhooks/deliveriesis the delivery log: status, response code, attempt count and next retry.

No public endpoint?Every event is also readable fromGET /events. Poll it withafterset to the last event id you handled. Events are retained for 30 days.

- activation.created— An activation was purchased and its number is ready to receive SMS.
- activation.message.received— An SMS arrived on an activation. The extracted verification code is indata.messages[0].codewhen one could be parsed.
- activation.completed— An activation was marked finished and will receive no further messages.
- activation.cancelled— An activation was cancelled before use and the balance was refunded.
- activation.expired— An activation reached its expiry window without receiving an SMS.
- activation.refunded— The balance for an activation was returned to the account.
- rental.created— A long-term rental was ordered. It may still be provisioning.
- rental.activated— A rental finished provisioning and is now live.
- rental.message.received— An SMS arrived on a rental number.
- rental.extended— A rental was extended and its expiry moved forward.
- rental.expiring— A rental expires within 24 hours.
- rental.expired— A rental reached the end of its period and stopped receiving messages.
- rental.cancelled— A rental was cancelled.
- balance.updated— The account balance changed.

Every failure returns the same envelope with a conventional HTTP status:

{ "error": { "type": "invalid_request_error", "code": "insufficient_balance", "message": "Insufficient balance. Required: 0.62, Available: 0.10", "doc_url": "https://www.smsz.net/api#errors", "request_id": "req_4f1c8a90b2d34e5f6a7b8c9d" } }

Branch oncode, not onmessage.Codes are stable; wording is not.paramnames the offending field when the error is about one.

Every response — success or failure — carries aSMSZ-Request-Idheader. Log it. Quoting it lets support find the exact request in seconds.

Every response carries your current standing:

RateLimit-Limit: 120 RateLimit-Remaining: 117 RateLimit-Reset: 1784889460

Exceeding a limit returns429withRetry-Afterin seconds. Wait that long rather than retrying tighter — hammering a 429 only extends it.

Need more? Emailsupport@smsz.netwith your key name and expected volume; the general limit is adjustable per key.

Network failures are ambiguous: a request that times out may or may not have bought a number. Send anIdempotency-Keyon anything that spends money and retrying becomes safe.

curl -X POST https://www.smsz.net/api/v1/activations \ -H "Authorization: Bearer $SMSZ_API_KEY" \ -H "Idempotency-Key: 3f9a1b7c-5d2e-4a8f-9c1b-2e7d4a6f8b3c" \ -H "Content-Type: application/json" \ -d '{"country": "US", "service": "telegram"}'

Retry with the same key and you getthe original response replayed, markedIdempotent-Replayed: true. No second purchase, no second charge.

- Use a fresh UUID per logical operation. Keys are remembered for24 hours.
- Reusing a key with a different body returnsidempotency_key_reused(422) — that is nearly always a bug where a constant was used instead of a per-request value.
- Retrying while the first attempt is still running returnsidempotency_request_in_progress(409). Wait a moment and try again.
- Only successful responses are stored. A failed request can be retried with the same key.

Supported onPOST /activations,POST /rentalsandPOST /rentals/{id}/extend.

List endpoints return a consistent envelope:

{ "object": "list", "data": [], "has_more": true, "total": 214 }

Page withlimit(1-100, default 25) andoffset:

curl "https://www.smsz.net/api/v1/activations?limit=50&offset=50" \ -H "Authorization: Bearer $SMSZ_API_KEY"

Lists are always newest-first.GET /eventsadditionally acceptsafter=<event id>, which is the right way to consume it as a stream — offsets shift as new events land, but a cursor does not.

Everything on this page is also available as aModel Context Protocolserver, so an AI assistant can buy numbers and read verification codes in conversation instead of through code.

It is a remote MCP server over Streamable HTTP, authenticated with the same API key as this API — same scopes, same rate limits, same balance. There is nothing to install and nothing to run locally.

Most clients need only the URL above and anAuthorization: Bearerheader:

Authorization: Bearer smsz_live_9f2a1c4d_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The full reference — every tool, connection instructions for each client, and the safety rules that matter when a language model is the one spending your balance — is at/mcp.

Before you connect a key that can buy anything, readSpending safely. The short version: scope the key to the narrowest set that covers the job, because a tool the key has no scope for is never even shown to the assistant, and that is the only safeguard that does not depend on a model behaving well.

If you are wiring up an assistant rather than writing a client, use theMCP server— it exposes all of this as tools, with the safety rules already written into the tool descriptions.

Machine-readable descriptions of this API:

The OpenAPI document is unauthenticated, so client generators and agents can read it before a key exists.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.