v2026-07-01REST · JSONWebhooks

SMSZ API

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.

Base URLhttps://www.smsz.net/api/v1

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

ActivationsRentals
PurposeOne verification codeOngoing use of a number
Lifetime~15-20 minutes1 day to 12 months
EndpointPOST /activationsPOST /rentals
MessagesUsually oneUnlimited 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:read
  • activations:read
  • activations:write
  • rentals:read
  • rentals:write
  • webhooks:read
  • webhooks: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.

StatusMeaning
pendingLive and waiting for an SMS
completedA message arrived, or you finished it
expiredThe window closed with no message — refunded
cancelledYou cancelled it
refundedThe 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 in data over 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 in data.messages[0].code when 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

StatusRetry?
400, 401, 402, 403, 404, 422No. Fix the request, the key, or your balance.
409Only idempotency_request_in_progress. The rest are state conflicts.
429Yes, after Retry-After.
5xxYes, with exponential backoff — and reuse the same Idempotency-Key.

Rate limits

Limits are per API key, per minute:

BucketLimitApplies to
General120/minReads and catalogue calls
Purchase20/minCreating, cancelling and extending
Webhook test10/minPOST /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.

For AI agents

Machine-readable descriptions of this API:

ResourceURL
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/activations before buying. Prices and stock move constantly; do not assume a country and service pair is available.
  • Always send an Idempotency-Key on purchases. If a call fails ambiguously, retry with the *same* key rather than issuing a new purchase.
  • Treat 402 insufficient_balance as terminal — a human has to top up. Do not retry it.
  • Respect Retry-After on 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" — use GET /ping for 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.

Scope: account:read

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.

Scope: account:read

ParameterInRequiredDescription
typequerynoFilter by transaction type.
statusquerynoFilter by status.
limitquerynoPage size, 1-100.
offsetquerynoRows 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.

Scope: activations:read

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.

Scope: activations:read

ParameterInRequiredDescription
countryquerynoCountry code, slug or name, e.g. US.
servicequerynoService 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

ParameterInRequiredDescription
countryquerynoFilter by ISO country code.
daysquerynoFilter 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 fieldTypeRequiredDescription
countrystringyesCountry code, slug or name, e.g. US.
servicestringyesService slug, e.g. telegram.
operatorstringnoOptional 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.

Scope: activations:read

ParameterInRequiredDescription
statusquerynoFilter by status.
limitquerynoPage size, 1-100.
offsetquerynoRows 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.

Scope: activations:read

ParameterInRequiredDescription
idpathyesActivation id.

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/{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.

Scope: activations:read

ParameterInRequiredDescription
idpathyesActivation id.

Response:

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

ParameterInRequiredDescription
idpathyesActivation id.

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

ParameterInRequiredDescription
idpathyesActivation id.

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 fieldTypeRequiredDescription
offer_idstringyesThe offer_id from GET /pricing/rentals. Offers expire after 30 minutes — fetch a fresh one if yours is rejected.
servicestringnoOptional. Rent one service on the number instead of the whole number, which is cheaper.
auto_renewbooleannoOptional. 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.

Scope: rentals:read

ParameterInRequiredDescription
statusquerynoFilter by status.
limitquerynoPage size, 1-100.
offsetquerynoRows to skip.

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.

Scope: rentals:read

ParameterInRequiredDescription
idpathyesRental id.

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/{id}/messages

List rental messages — Every message received on the rental, newest first. Polls the provider before answering.

Scope: rentals:read

ParameterInRequiredDescription
idpathyesRental id.

Response:

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

Scope: rentals:write · Supports Idempotency-Key · Purchase rate limit

ParameterInRequiredDescription
idpathyesRental id.
Body fieldTypeRequiredDescription
daysintegeryesDays to add, 1-365.
auto_renewbooleannoOptional. 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

ParameterInRequiredDescription
idpathyesRental id.

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 fieldTypeRequiredDescription
urlstringyesHTTPS URL to deliver to. Must be publicly reachable.
eventsarraynoEvent types to subscribe to. Defaults to ["*"] (everything).
descriptionstringnoOptional 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.

Scope: webhooks:read

ParameterInRequiredDescription
idpathyesEndpoint 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.

Scope: webhooks:write

ParameterInRequiredDescription
idpathyesEndpoint id.
Body fieldTypeRequiredDescription
urlstringnoNew HTTPS URL.
eventsarraynoReplacement subscription list.
descriptionstringnoNew label.
statusstringnoEnable 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.

Scope: webhooks:write

ParameterInRequiredDescription
idpathyesEndpoint id.

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.

Scope: webhooks:write

ParameterInRequiredDescription
idpathyesEndpoint id.

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.

Scope: webhooks:read

ParameterInRequiredDescription
endpoint_idquerynoFilter to one endpoint.
statusquerynoFilter by delivery status.
limitquerynoPage size, 1-100.
offsetquerynoRows 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.

Scope: webhooks:read

ParameterInRequiredDescription
typequerynoFilter by event type.
object_idquerynoFilter to one activation or rental.
afterquerynoReturn only events after this event id.
limitquerynoPage size, 1-100.
offsetquerynoRows to skip.

Error codes

CodeHTTPMeaning
missing_api_key401No Authorization header was sent.
invalid_api_key401The key is not recognised.
expired_api_key401The key passed its expiry date.
revoked_api_key401The key was revoked in the dashboard.
insufficient_scope403The key lacks the scope this endpoint needs.
ip_not_allowed403The calling IP is not on the key’s allowlist.
account_blocked403The account cannot use the API.
invalid_body400The request body was not valid JSON.
missing_parameter400A required field was absent. See `param`.
invalid_parameter400A field was unusable. See `param`.
insufficient_balance402Your balance does not cover the purchase.
resource_not_found404No such object on this account.
number_unavailable409No stock for that country and service right now.
not_cancellable409Outside the cancellation window, or already used.
not_extendable409That extension length is not offered for this rental.
resource_conflict409The object is not in a state that allows this.
idempotency_request_in_progress409The same Idempotency-Key is still running. Retry shortly.
idempotency_key_reused422That Idempotency-Key was used with a different body.
rate_limit_exceeded429Too many requests. See `Retry-After`.
internal_error500Our fault. Quote the `request_id` to support.
provider_rejected502An upstream provider refused the order.
provider_unavailable503An 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.