Receive SMS online
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.
文件
Contentsshow
Introduction
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.
Base URL
https://www.smsz.net/api/v1
Everything is JSON over HTTPS. All amounts are in USD, all timestamps are ISO 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 is 2026-07-01, returned on every response as the SMSZ-Version header. Additive changes (new fields, new endpoints, new event types) ship without a version bump, so write clients that ignore unknown fields.
Two products
| Activations | Rentals | |
|---|---|---|
| Purpose | One verification code | Ongoing use of a number |
| Lifetime | ~15-20 minutes | 1 day to 12 months |
| Endpoint | POST /activations | POST /rentals |
| Messages | Usually one | Unlimited for the period |
Authentication
Every request carries an API key as a bearer token:
Authorization: Bearer smsz_live_9f2a1c4d_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Creating a key. Sign in at smsz.net, open the account menu and choose API keys. The full key is shown once, at creation — we store only a hash of it, so if you lose it you must create a new one.
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 hold webhooks:write.
account:readactivations:readactivations:writerentals:readrentals:writewebhooks:readwebhooks:write
A call missing the scope it needs fails with insufficient_scope and 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 with ip_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.
Verify a new key with:
curl https://www.smsz.net/api/v1/ping \
-H "Authorization: Bearer $SMSZ_API_KEY"
Quickstart
Buying a number and reading its code takes two calls.
1. Buy the number
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": []
}
Use phone_number wherever you are signing up. Your balance is debited immediately; if the provider cannot fill the order you are not charged at all.
2. Get the code
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 until data is non-empty, or use a webhook and skip polling entirely. code is the digits we extracted; text is the full message if the extraction misses.
3. Finish
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.
A complete example
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")
Activations
An activation is a number rented for a single verification. It lives for roughly 15-20 minutes depending on the provider, and expires_at tells you exactly when.
Choosing what to buy. country accepts an ISO code, our slug, or the country name — US, united-states and United States all work. service is a slug from GET /services. Check price and stock first with GET /pricing/activations?country=US&service=telegram.
Omit operator and we pick the cheapest one with stock. Omit provider too — pinning a provider only narrows what we can fill the order from.
Status.
| Status | Meaning |
|---|---|
| pending | Live and waiting for an SMS |
| completed | A message arrived, or you finished it |
| expired | The window closed with no message — refunded |
| cancelled | You cancelled it |
| refunded | The charge was returned |
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 check GET /pricing/activations for what is actually available.
Rentals
A rental holds a number for days or months and receives unlimited messages for the period.
Ordering. Rentals are bought against an *offer* from GET /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's offer_id straight 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_id is opaque and short-lived: treat it as a token to round-trip, not a value to parse or store. It expires after 30 minutes, so fetch offers immediately before ordering rather than caching them. An expired or altered token is rejected with invalid_parameter — fetch a fresh one and retry.
Per-service rentals. Adding service rents one service on the number rather than the whole number. Cheaper when you only need one platform.
Extending. POST /rentals/{id}/extend with {"days": 30} adds time and charges your balance. Extend before expires_at — an expired rental cannot be revived, only replaced.
Cancelling. Rentals are refundable within 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 returns not_cancellable.
Messages. Use GET /rentals/{id}/messages for the full history, or subscribe to rental.message.received and be pushed each one. Rentals often run for weeks, so webhooks are strongly preferred over polling here.
Webhooks
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.
Register an endpoint
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 a secret starting whsec_. This is the only time it is returned. Store it — it is what proves a delivery came from us.
Subscribe to ["*"] for everything, or use prefixes like ["rental.*"] for a family of events.
Payload shape
{
"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" }
]
}
}
data is the same object the REST endpoints return, so one deserializer handles both.
Verifying signatures
Every delivery carries a SMSZ-Signature header:
SMSZ-Signature: t=1784889404,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
v1 is HMAC-SHA256 of {timestamp}.{raw request body}, keyed with your endpoint secret. Verify it on the 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
Delivery rules
- Any 2xx is an acknowledgement. Anything else is retried.
- Retries: 8 attempts at 10s → 30s → 2m → 10m → 30m → 2h → 6h → 12h — just over 24 hours in total.
- Deliveries time out after 10 seconds, so acknowledge immediately and do the work asynchronously.
- Redirects are not followed. If your URL moves, update the endpoint.
- After 20 consecutive failures an endpoint is disabled. Re-enable it with
PATCH /webhooks/endpoints/{id}and{"status": "active"}. - Delivery is at-least-once and ordering is not guaranteed. Deduplicate on the event
id, and prefer the state indataover inferring it from event sequence.
Debugging. POST /webhooks/endpoints/{id}/test sends a real signed event with obviously fake data and reports what your server answered. GET /webhooks/deliveries is the delivery log: status, response code, attempt count and next retry.
No public endpoint? Every event is also readable from GET /events. Poll it with after set to the last event id you handled. Events are retained for 30 days.
Event types
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.
Errors
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 on code, not on message. Codes are stable; wording is not. param names the offending field when the error is about one.
Every response — success or failure — carries a SMSZ-Request-Id header. Log it. Quoting it lets support find the exact request in seconds.
Which errors to retry
| Status | Retry? |
|---|---|
| 400, 401, 402, 403, 404, 422 | No. Fix the request, the key, or your balance. |
| 409 | Only idempotency_request_in_progress. The rest are state conflicts. |
| 429 | Yes, after Retry-After. |
| 5xx | Yes, with exponential backoff — and reuse the same Idempotency-Key. |
Rate limits
Limits are per API key, per minute:
| Bucket | Limit | Applies to |
|---|---|---|
| General | 120/min | Reads and catalogue calls |
| Purchase | 20/min | Creating, cancelling and extending |
| Webhook test | 10/min | POST /webhooks/endpoints/{id}/test |
Every response carries your current standing:
RateLimit-Limit: 120
RateLimit-Remaining: 117
RateLimit-Reset: 1784889460
Exceeding a limit returns 429 with Retry-After in seconds. Wait that long rather than retrying tighter — hammering a 429 only extends it.
Need more? Email support@smsz.net with your key name and expected volume; the general limit is adjustable per key.
Idempotency
Network failures are ambiguous: a request that times out may or may not have bought a number. Send an Idempotency-Key on 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 get the original response replayed, marked Idempotent-Replayed: true. No second purchase, no second charge.
- Use a fresh UUID per logical operation. Keys are remembered for 24 hours.
- Reusing a key with a *different* body returns
idempotency_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 returns
idempotency_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 on POST /activations, POST /rentals and POST /rentals/{id}/extend.
Pagination
List endpoints return a consistent envelope:
{
"object": "list",
"data": [],
"has_more": true,
"total": 214
}
Page with limit (1-100, default 25) and offset:
curl "https://www.smsz.net/api/v1/activations?limit=50&offset=50" \
-H "Authorization: Bearer $SMSZ_API_KEY"
Lists are always newest-first. GET /events additionally accepts after=<event id>, which is the right way to consume it as a stream — offsets shift as new events land, but a cursor does not.
MCP server
Everything on this page is also available as a Model Context Protocol server, so an AI assistant can buy numbers and read verification codes in conversation instead of through code.
https://www.smsz.net/api/mcp
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 an Authorization: Bearer header:
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, read Spending 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.
For AI agents
If you are wiring up an assistant rather than writing a client, use the MCP server — it exposes all of this as tools, with the safety rules already written into the tool descriptions.
Machine-readable descriptions of this API:
| Resource | URL |
|---|---|
| MCP server | /mcp — https://www.smsz.net/api/mcp |
| OpenAPI 3.1 specification | /api/v1/openapi.json |
| These docs as one Markdown file | /api/llms-full.txt |
| Short index for model context | /llms.txt |
The OpenAPI document is unauthenticated, so client generators and agents can read it before a key exists.
Notes for autonomous callers
- Read
GET /pricing/activationsbefore buying. Prices and stock move constantly; do not assume a country and service pair is available. - Always send an
Idempotency-Keyon purchases. If a call fails ambiguously, retry with the *same* key rather than issuing a new purchase. - Treat 402
insufficient_balanceas terminal — a human has to top up. Do not retry it. - Respect
Retry-Afteron 429. Do not retry 4xx errors other than 429. - An activation costs real money on every successful
POST /activations. There is no test mode; do not call it to "check whether it works" — useGET /pingfor that.
Account endpoints
GET/ping
Verify your API key — Confirms a key is valid and reports which account and scopes it carries. Make this your first call when setting up an integration.
Scope: account:read
Response:
{
"object": "ping",
"ok": true,
"api_version": "2026-07-01",
"account_email": "you@example.com",
"key_name": "Production server",
"scopes": [
"account:read",
"activations:write"
]
}
GET/account
Retrieve your account — Returns your current balance and how many activations and rentals are live.
Response:
{
"object": "account",
"id": "cmg7w1a2b0000l208ff11aaaa",
"email": "you@example.com",
"username": null,
"balance": 42.75,
"currency": "USD",
"created_at": "2026-01-04T08:00:00.000Z",
"active_activations": 1,
"active_rentals": 2
}
GET/transactions
List transactions — Your ledger: purchases, refunds and deposits, newest first.
| Parameter | In | Required | Description |
|---|---|---|---|
| type | query | no | Filter by transaction type. |
| status | query | no | Filter by status. |
| limit | query | no | Page size, 1-100. |
| offset | query | no | Rows to skip. |
Response:
{
"object": "list",
"data": [
{
"id": "cmg7xd4u90008l208p7q1rstu",
"object": "transaction",
"type": "sms_purchase",
"status": "completed",
"amount": -0.62,
"currency": "USD",
"description": "SMS purchase for telegram (US)",
"created_at": "2026-07-24T10:15:03.000Z"
}
],
"has_more": false,
"total": 1
}
Catalogue endpoints
GET/countries
List countries — Every country we sell numbers in. Use code when creating an activation.
Scope: activations:read
Response:
{
"object": "list",
"data": [
{
"code": "US",
"name": "United States",
"full_name": "United States of America",
"slug": "united-states",
"phone_code": "+1",
"continent": "north_america"
}
],
"has_more": false,
"total": 1
}
GET/services
List services — Every service you can verify. Use slug when creating an activation.
Response:
{
"object": "list",
"data": [
{
"slug": "telegram",
"name": "Telegram",
"full_name": "Telegram Messenger",
"popular": true
}
],
"has_more": false,
"total": 1
}
GET/pricing/activations
Live activation prices — Current prices and stock, queried live from our providers. You must pass country, service, or both — an unfiltered sweep would price every country against every service.
| Parameter | In | Required | Description |
|---|---|---|---|
| country | query | no | Country code, slug or name, e.g. US. |
| service | query | no | Service slug, e.g. telegram. |
Response:
{
"object": "list",
"data": [
{
"country": "US",
"country_name": "United States",
"service": "telegram",
"service_name": "Telegram",
"price": 0.62,
"currency": "USD",
"available": 418,
"success_rate": 92
}
],
"has_more": false,
"total": 1
}
GET/pricing/rentals
Live rental offers — Purchasable long-term rental offers. Pass an offer’s offer_id straight to POST /rentals — it is an opaque, short-lived token that carries everything needed to fill the order.
Scope: rentals:read
| Parameter | In | Required | Description |
|---|---|---|---|
| country | query | no | Filter by ISO country code. |
| days | query | no | Filter by rental length in days. |
Response:
{
"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
}
],
"has_more": false,
"total": 1
}
Activations endpoints
POST/activationsidempotent
Buy an activation — Buys a number for one verification and debits your balance. If the provider cannot fill the order, nothing is charged. Poll /activations/{id}/messages for the code, or subscribe to activation.message.received.
Scope: activations:write · Supports Idempotency-Key · Purchase rate limit
| Body field | Type | Required | Description |
|---|---|---|---|
| country | string | yes | Country code, slug or name, e.g. US. |
| service | string | yes | Service slug, e.g. telegram. |
| operator | string | no | Optional operator. Omit to let us pick the cheapest available. |
Request:
{
"country": "US",
"service": "telegram"
}
Response:
{
"id": "cmg7x2k9a0001l208hq3v7bqz",
"object": "activation",
"status": "pending",
"phone_number": "+12025550147",
"country": "US",
"country_name": "United States",
"service": "telegram",
"service_name": "Telegram",
"operator": "any",
"price": 0.62,
"currency": "USD",
"created_at": "2026-07-24T10:15:03.000Z",
"expires_at": "2026-07-24T10:30:03.000Z",
"messages": []
}
GET/activations
List activations — Your activations, newest first, each with any messages received.
| Parameter | In | Required | Description |
|---|---|---|---|
| status | query | no | Filter by status. |
| limit | query | no | Page size, 1-100. |
| offset | query | no | Rows to skip. |
Response:
{
"object": "list",
"data": [
{
"id": "cmg7x2k9a0001l208hq3v7bqz",
"object": "activation",
"status": "pending",
"phone_number": "+12025550147",
"country": "US",
"country_name": "United States",
"service": "telegram",
"service_name": "Telegram",
"operator": "any",
"price": 0.62,
"currency": "USD",
"created_at": "2026-07-24T10:15:03.000Z",
"expires_at": "2026-07-24T10:30:03.000Z",
"messages": []
}
],
"has_more": false,
"total": 1
}
GET/activations/{id}
Retrieve an activation — One activation and its messages.
| Parameter | In | Required | Description |
|---|---|---|---|
| id | path | yes | Activation id. |
Response:
GET/activations/{id}/messages
Poll for the code — Asks the provider directly, so a message that has not reached us by webhook yet still appears. Poll every 3-5 seconds; webhooks are the better integration if you can host an endpoint.
Response:
POST/activations/{id}/cancel
Cancel an activation — Cancels an unused activation and refunds it. Once a message has arrived the activation has delivered what it was bought for, so the cancellation succeeds with refund_amount: 0.
Scope: activations:write · Purchase rate limit
Response:
{
"id": "cmg7x2k9a0001l208hq3v7bqz",
"object": "activation",
"status": "refunded",
"phone_number": "+12025550147",
"country": "US",
"country_name": "United States",
"service": "telegram",
"service_name": "Telegram",
"operator": "any",
"price": 0.62,
"currency": "USD",
"created_at": "2026-07-24T10:15:03.000Z",
"expires_at": "2026-07-24T10:30:03.000Z",
"messages": [],
"refund_amount": 0.62,
"refund_reason": "Full refund - No SMS received"
}
POST/activations/{id}/finish
Finish an activation — Closes an activation once you have used the code, releasing the number back to the provider. If no message ever arrived, finishing also refunds the purchase.
Scope: activations:write
Response:
{
"id": "cmg7x2k9a0001l208hq3v7bqz",
"object": "activation",
"status": "completed",
"phone_number": "+12025550147",
"country": "US",
"country_name": "United States",
"service": "telegram",
"service_name": "Telegram",
"operator": "any",
"price": 0.62,
"currency": "USD",
"created_at": "2026-07-24T10:15:03.000Z",
"expires_at": "2026-07-24T10:30:03.000Z",
"messages": [],
"refund_amount": 0
}
Rentals endpoints
POST/rentalsidempotent
Order a rental — Rents a number for days or months. Take an offer_id from GET /pricing/rentals and pass it back — the offer already fixes the country, length and price.
Scope: rentals:write · Supports Idempotency-Key · Purchase rate limit
| Body field | Type | Required | Description |
|---|---|---|---|
| offer_id | string | yes | The offer_id from GET /pricing/rentals. Offers expire after 30 minutes — fetch a fresh one if yours is rejected. |
| service | string | no | Optional. Rent one service on the number instead of the whole number, which is cheaper. |
| auto_renew | boolean | no | Optional. Renew automatically at expiry, where the offer supports it. |
Request:
{
"offer_id": "o1.Xn9pQ2s.7Kd1fA.k3mZq0vR8tYw"
}
Response:
{
"id": "cmg7x9p2r0004l208d1w4kzab",
"object": "rental",
"status": "active",
"phone_number": "+447700900123",
"country": "GB",
"country_name": "United Kingdom",
"service": "full",
"service_name": "Full Rent",
"nickname": null,
"auto_renew": false,
"price": 14.5,
"currency": "USD",
"created_at": "2026-07-24T10:20:11.000Z",
"expires_at": "2026-08-23T10:20:11.000Z",
"messages": []
}
GET/rentals
List rentals — Your long-term rentals, newest first.
Response:
{
"object": "list",
"data": [
{
"id": "cmg7x9p2r0004l208d1w4kzab",
"object": "rental",
"status": "active",
"phone_number": "+447700900123",
"country": "GB",
"country_name": "United Kingdom",
"service": "full",
"service_name": "Full Rent",
"nickname": null,
"auto_renew": false,
"price": 14.5,
"currency": "USD",
"created_at": "2026-07-24T10:20:11.000Z",
"expires_at": "2026-08-23T10:20:11.000Z",
"messages": []
}
],
"has_more": false,
"total": 1
}
GET/rentals/{id}
Retrieve a rental — One rental and its messages.
| Parameter | In | Required | Description |
|---|---|---|---|
| id | path | yes | Rental id. |
Response:
GET/rentals/{id}/messages
List rental messages — Every message received on the rental, newest first. Polls the provider before answering.
Response:
POST/rentals/{id}/extendidempotent
Extend a rental — Adds time to a live rental and charges your balance. Expired rentals cannot be extended — order a new one.
| Body field | Type | Required | Description |
|---|---|---|---|
| days | integer | yes | Days to add, 1-365. |
| auto_renew | boolean | no | Optional. Renew automatically at expiry, where the rental supports it. |
Request:
{
"days": 30
}
Response:
{
"id": "cmg7x9p2r0004l208d1w4kzab",
"object": "rental",
"status": "active",
"phone_number": "+447700900123",
"country": "GB",
"country_name": "United Kingdom",
"service": "full",
"service_name": "Full Rent",
"nickname": null,
"auto_renew": false,
"price": 14.5,
"currency": "USD",
"created_at": "2026-07-24T10:20:11.000Z",
"expires_at": "2026-08-23T10:20:11.000Z",
"messages": [],
"extended_hours": 720,
"amount_charged": 14.5
}
POST/rentals/{id}/cancel
Cancel a rental — Cancels and refunds a rental. Only possible within 120 minutes of purchase and only if no message has been received — that is the window our providers give us.
Scope: rentals:write · Purchase rate limit
Response:
{
"id": "cmg7x9p2r0004l208d1w4kzab",
"object": "rental",
"status": "refunded",
"phone_number": "+447700900123",
"country": "GB",
"country_name": "United Kingdom",
"service": "full",
"service_name": "Full Rent",
"nickname": null,
"auto_renew": false,
"price": 14.5,
"currency": "USD",
"created_at": "2026-07-24T10:20:11.000Z",
"expires_at": "2026-08-23T10:20:11.000Z",
"messages": [],
"refund_amount": 14.5,
"refund_reason": "Full refund - No SMS received"
}
Webhooks endpoints
POST/webhooks/endpoints
Create a webhook endpoint — Registers an HTTPS URL to receive events. The response contains the signing secret — this is the only time it is returned, so store it now.
Scope: webhooks:write
| Body field | Type | Required | Description |
|---|---|---|---|
| url | string | yes | HTTPS URL to deliver to. Must be publicly reachable. |
| events | array | no | Event types to subscribe to. Defaults to ["*"] (everything). |
| description | string | no | Optional label, up to 160 characters. |
Request:
{
"url": "https://example.com/hooks/smsz",
"events": [
"activation.message.received",
"rental.message.received"
],
"description": "Production listener"
}
Response:
{
"id": "cmg7xf7w1000al208z3a5vwxy",
"object": "webhook_endpoint",
"url": "https://example.com/hooks/smsz",
"description": "Production listener",
"events": [
"activation.message.received",
"rental.message.received"
],
"status": "active",
"api_version": "2026-07-01",
"secret": "whsec_p9Qk…",
"created_at": "2026-07-24T10:30:00.000Z",
"last_success_at": null,
"last_error_at": null,
"last_error": null
}
GET/webhooks/endpoints
List webhook endpoints — Your configured endpoints. Secrets are never included.
Scope: webhooks:read
Response:
{
"object": "list",
"data": [],
"has_more": false,
"total": 0
}
GET/webhooks/endpoints/{id}
Retrieve a webhook endpoint — One endpoint, including when it last succeeded or failed.
| Parameter | In | Required | Description |
|---|---|---|---|
| id | path | yes | Endpoint id. |
PATCH/webhooks/endpoints/{id}
Update a webhook endpoint — Change the URL, subscriptions or description. Send status: "active" to re-enable an endpoint we disabled after repeated failures — this also resets its failure counter.
| Body field | Type | Required | Description |
|---|---|---|---|
| url | string | no | New HTTPS URL. |
| events | array | no | Replacement subscription list. |
| description | string | no | New label. |
| status | string | no | Enable or disable the endpoint. One of: active, disabled. |
Request:
{
"events": [
"*"
]
}
DELETE/webhooks/endpoints/{id}
Delete a webhook endpoint — Removes the endpoint and any deliveries still queued for it.
Response:
{
"id": "cmg7xf7w1000al208z3a5vwxy",
"object": "webhook_endpoint",
"deleted": true
}
POST/webhooks/endpoints/{id}/test
Send a test event — Delivers a real, correctly signed event with obviously fake data, and reports exactly what your endpoint answered. Use it to verify signature checking before going live.
Response:
{
"object": "webhook_test",
"endpoint_id": "cmg7xf7w1000al208z3a5vwxy",
"delivered": true,
"response_status": 200,
"duration_ms": 143,
"error": null
}
GET/webhooks/deliveries
List deliveries — What was sent, what your endpoint answered, how many attempts it took and when the next retry is due. Start here when events are not arriving.
| Parameter | In | Required | Description |
|---|---|---|---|
| endpoint_id | query | no | Filter to one endpoint. |
| status | query | no | Filter by delivery status. |
| limit | query | no | Page size, 1-100. |
| offset | query | no | Rows to skip. |
GET/events
List events — Every event on your account, whether or not you have a webhook endpoint. Poll this with after set to the last event id you processed if you cannot host an endpoint. Retained for 30 days.
| Parameter | In | Required | Description |
|---|---|---|---|
| type | query | no | Filter by event type. |
| object_id | query | no | Filter to one activation or rental. |
| after | query | no | Return only events after this event id. |
| limit | query | no | Page size, 1-100. |
| offset | query | no | Rows to skip. |
Error codes
| Code | HTTP | Meaning |
|---|---|---|
| missing_api_key | 401 | No Authorization header was sent. |
| invalid_api_key | 401 | The key is not recognised. |
| expired_api_key | 401 | The key passed its expiry date. |
| revoked_api_key | 401 | The key was revoked in the dashboard. |
| insufficient_scope | 403 | The key lacks the scope this endpoint needs. |
| ip_not_allowed | 403 | The calling IP is not on the key’s allowlist. |
| account_blocked | 403 | The account cannot use the API. |
| invalid_body | 400 | The request body was not valid JSON. |
| missing_parameter | 400 | A required field was absent. See `param`. |
| invalid_parameter | 400 | A field was unusable. See `param`. |
| insufficient_balance | 402 | Your balance does not cover the purchase. |
| resource_not_found | 404 | No such object on this account. |
| number_unavailable | 409 | No stock for that country and service right now. |
| not_cancellable | 409 | Outside the cancellation window, or already used. |
| not_extendable | 409 | That extension length is not offered for this rental. |
| resource_conflict | 409 | The object is not in a state that allows this. |
| idempotency_request_in_progress | 409 | The same Idempotency-Key is still running. Retry shortly. |
| idempotency_key_reused | 422 | That Idempotency-Key was used with a different body. |
| rate_limit_exceeded | 429 | Too many requests. See `Retry-After`. |
| internal_error | 500 | Our fault. Quote the `request_id` to support. |
| provider_rejected | 502 | An upstream provider refused the order. |
| provider_unavailable | 503 | An upstream provider is temporarily down. |
Something missing?
Email support@smsz.net with your request id and we will take a look. Include the SMSZ-Request-Id header from the failing response — it points straight at the request in our logs.