Przegląd
API dla deweloperów sprzedaje te same numery co aplikacja, z tego samego salda, przez REST API i zgodny punkt końcowy.
- Base URL
https://smsgrab.com/api/dev/v1- sms-activate compatible
https://smsgrab.com/api/stubs/handler_api.php- Format
- JSON over HTTPS,
snake_casefields, UTF-8. Money is an integer in minor units ofcurrency(USD cents). Timestamps are ISO 8601 in UTC with milliseconds. - Authentication
- An API key (
sgk_…) inAuthorization: BearerorX-Api-Key. - Money
- Every charge goes through the same double-entry ledger as the app and the website. Numbers without a code are refunded automatically.
- Request ids
- Every answer carries
X-Request-Id. Include it when you contact support.
Szybki start
Trzy wywołania prowadzą od nowego klucza do otrzymanego kodu.
- Załóż konto ze zweryfikowanym adresem e-mail i otwórz strefę dewelopera.
- Utwórz klucz z dostępem „Odczyt i zakupy” i skopiuj jego sekret.
- Upewnij się, że saldo pokryje wybrany numer, a potem wykonaj poniższe wywołania.
export SMSGRAB_API_KEY="sgk_your_key"
curl -s https://smsgrab.com/api/dev/v1/balance \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
curl -s -X POST https://smsgrab.com/api/dev/v1/activations \
-H "Authorization: Bearer $SMSGRAB_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"service":"telegram","country":"indonesia","max_price_minor":30}'
curl -s "https://smsgrab.com/api/dev/v1/activations/100000123?wait=25" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
curl -s -X POST https://smsgrab.com/api/dev/v1/activations/100000123/finish \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
The number is yours for 20 minutes. Poll GET /activations/{id}?wait=25 (the request is held until something changes) or subscribe to webhooks. When a code has arrived, finish the activation; if none arrives, it expires and the charge is refunded automatically. Complete programs in five languages are in Pełne przykłady.
Uwierzytelnianie
Każde żądanie zawiera jeden z Twoich kluczy API.
GET /api/dev/v1/balance HTTP/1.1
Host: smsgrab.com
Authorization: Bearer sgk_your_key
X-Api-Key: sgk_… works as well. A key in a query string is accepted only by the compatible endpoint. There is no CORS: keys belong on servers, never in browsers or apps you ship to users.
| Situation | Answer |
|---|---|
| No key, malformed or unknown key | 401 UNAUTHORIZED · API_KEY_INVALID |
| Revoked key | 401 UNAUTHORIZED · API_KEY_REVOKED |
| Expired key, or an old secret after its rotation grace | 401 UNAUTHORIZED · API_KEY_EXPIRED |
| Address outside the key's allowlist | 403 FORBIDDEN · API_KEY_IP_NOT_ALLOWED |
| The key lacks the needed scope | 403 FORBIDDEN · API_KEY_SCOPE |
| E-mail no longer verified, account suspended, or developer access blocked | 403 FORBIDDEN · EMAIL_NOT_VERIFIED, ACCOUNT_BLOCKED, DEVELOPER_ACCESS_BLOCKED |
| Too many rejected keys from one network | 429 RATE_LIMITED |
Klucze API
Klucze tworzysz, ograniczasz i wymieniasz na swoim koncie.
| Format | sgk_ followed by 40 letters and digits. Shown once, when it is created or rotated. We store only a keyed hash. |
|---|---|
| Scopes | read (balance, catalogue, prices, activations, usage) or read + purchase (buy, cancel, finish). |
| IP allowlist | Up to 50 IPv4/IPv6 addresses or CIDR ranges. Empty means any address. |
| Daily spend cap | Optional, per UTC day, counted over the key's net charges. Purchases above it answer 403 DAILY_SPEND_CAP_REACHED and nothing is charged. |
| Expiry | Optional, at most two years ahead. |
| Rotation | A new secret for the same key, settings, usage and webhooks. The old secret keeps working for the grace period you choose (up to 72 hours). |
| Revocation | Immediate and final. The key's webhooks are switched off. |
| Per account | Up to 10 active keys. |
Creating or rotating a key, adding a webhook, rolling its secret and widening a key's access (more scopes, a wider allowlist, a higher cap or a later expiry) ask for your password again, and your two-step code when it is on. Revoking and tightening never do.
Ceny
Twoja cena składa się z kosztu hurtowego każdej oferty i marży, która maleje z wolumenem.
developer_price = max(ceil(wholesale × (100 + markup) / 100), minimum)
developer_price = min(developer_price, app_price)
| Wydatki deweloperskie z 30 dni | Marża na cenie hurtowej | Przykładowa cena |
|---|---|---|
| Poniżej 100,00 USD | +80% | 0,18 USD o 28% taniej niż w aplikacji |
| Od 100,00 USD | +70% | 0,17 USD o 32% taniej niż w aplikacji |
| Od 500,00 USD | +60% | 0,16 USD o 36% taniej niż w aplikacji |
| Od 2 000,00 USD | +50% | 0,15 USD o 40% taniej niż w aplikacji |
The tier follows your trailing 30-day developer spend (charges minus refunds of numbers bought through the API or the compatible endpoint) and applies within five minutes of crossing a threshold. The minimum price is 0,10 USD. A developer price is never above the price of the same number in the app. The price is fixed at purchase time; send max_price_minor to protect yourself from a change between reading prices and buying. Your current markup and tier are in GET /me.
Limity częstotliwości
Limity utrzymują szybkość platformy dla wszystkich. Większość integracji nigdy ich nie osiąga.
| Limit | Default | When exceeded |
|---|---|---|
| Requests per key | 600 / minute | 429 RATE_LIMITED with Retry-After |
| Purchases per key | 60 / minute | 429 RATE_LIMITED |
| Requests per account (all keys) | 3 000 / minute | 429 RATE_LIMITED |
| Numbers waiting at once | 100 | 422 VALIDATION · ACTIVATION_LIMIT_REACHED |
| Held long-poll requests per key | 20 | 429 RATE_LIMITED |
| Webhooks per key | 3 | 422 VALIDATION · WEBHOOK_LIMIT_REACHED |
Every answer carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset and RateLimit-Policy for the tightest limit that applies.
Błędy
Nieudane wywołania odpowiadają stałym kodem i, gdy to pomocne, dokładniejszą przyczyną.
{
"code": "NO_NUMBERS_AVAILABLE",
"message": "No numbers available for this service and country",
"reason": "OUT_OF_STOCK"
}
| HTTP | code | reason | Meaning |
|---|---|---|---|
400 / 422 | VALIDATION | ACTIVATION_LIMIT_REACHED, ACTIVATION_CODE_RECEIVED, ACTIVATION_FINISHED, ACTIVATION_NO_CODE_YET, ACTIVATION_CANCEL_NOT_YET | A field is malformed or the activation is in the wrong state. The message names the field. |
401 | UNAUTHORIZED | API_KEY_INVALID, API_KEY_REVOKED, API_KEY_EXPIRED | No key, an unknown key, a revoked key, or an old secret after its rotation grace. |
402 | INSUFFICIENT_BALANCE | – | The balance does not cover the price. Nothing is charged. |
403 | FORBIDDEN | API_KEY_IP_NOT_ALLOWED, API_KEY_SCOPE, EMAIL_NOT_VERIFIED, ACCOUNT_BLOCKED, DEVELOPER_ACCESS_BLOCKED, DAILY_SPEND_CAP_REACHED | The key or the account may not do this right now. |
404 | NOT_FOUND, SERVICE_NOT_FOUND, COUNTRY_NOT_FOUND | ACTIVATION_NOT_FOUND | Unknown id, service or country. |
409 | NO_NUMBERS_AVAILABLE | OUT_OF_STOCK, PROVIDER_OUT_OF_NUMBERS | No number could be delivered. Any charge was refunded in the same request. |
409 | PRICE_CHANGED | – | The price is above max_price_minor. Nothing is charged. |
429 | RATE_LIMITED | – | Slow down and retry after the Retry-After header. |
503 | PROVIDER_UNAVAILABLE | PURCHASES_PAUSED, PROVIDER_PAUSED, PROVIDER_CIRCUIT_OPEN, SERVER_DRAINING | Purchases are paused for a moment. Nothing is charged. |
503 | FEATURE_UNAVAILABLE | – | The developer API is switched off for maintenance. |
5xx | INTERNAL | – | Retry idempotent requests with exponential backoff. |
New reason values may be added at any time; treat unknown ones like their code.
Idempotencja
Bezpieczne ponowienie zakupu nigdy nie kupuje dwa razy.
Send an Idempotency-Key header (any unique string up to 64 characters, a UUID is ideal) with POST /activations. If the network drops and you retry with the same key, you get the first answer again with Idempotent-Replayed: true instead of a second number. Cancel and finish are idempotent by nature: repeating them when the activation already has that status succeeds.
Punkty końcowe
Wszystkie punkty końcowe są pod adresem bazowym i odpowiadają w JSON.
- GET
/meWywołujący klucz, Twoja cena i limity. - GET
/balanceSaldo Twojego konta. - GET
/countriesKraje z dostępnymi numerami. - GET
/servicesUsługi, opcjonalnie tylko dostępne w jednym kraju. - GET
/pricesTwoje ceny deweloperskie i zapas dla każdej oferty. - POST
/activationsKup numer. - GET
/activationsWyświetl swoje aktywacje. - GET
/activations/{id}Jedna aktywacja, z opcjonalnym long pollingiem. - POST
/activations/{id}/cancelAnuluj przed nadejściem kodu i odzyskaj opłatę. - POST
/activations/{id}/finishZakończ aktywację po otrzymaniu kodu. - GET
/usageŻądania, zakupy i wydatki w czasie.
GET
/me
Wywołujący klucz, Twoja cena i limity. uprawnienie: read
curl -s https://smsgrab.com/api/dev/v1/me \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{
"key": { "id": "0b8e2a41-7c1d-4e5f-9a3b-6c7d8e9f0a1b", "name": "production bot", "display_prefix": "sgk_4hT9wQ", "last4": "Xa7Q",
"scopes": ["read", "purchase"], "ip_allowlist": ["203.0.113.0/24"], "daily_spend_cap_minor": 5000,
"rate_limit_per_minute": 600, "purchase_limit_per_minute": 60, "expires_at": null },
"account": { "user_id": "2b4b9c1d-3e5f-4a6b-8c7d-9e0f1a2b3c4d", "email": "dev@example.org" },
"currency": "USD",
"pricing": { "markup_percent": 70, "source": "tier", "min_price_minor": 10, "cap_at_retail": true,
"trailing_30d_spend_minor": 18250,
"tier": { "min_spend_minor": 10000, "markup_percent": 70 },
"next_tier": { "min_spend_minor": 50000, "markup_percent": 60, "remaining_minor": 31750 } },
"limits": { "max_active_activations": 100, "active_activations": 3, "spend_today_minor": 420 }
}
GET
/balance
Saldo Twojego konta. uprawnienie: read
curl -s https://smsgrab.com/api/dev/v1/balance \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "balance_minor": 12345, "currency": "USD", "updated_at": "2026-09-27T10:02:11.482Z" }
GET
/countries
Kraje z dostępnymi numerami. uprawnienie: read
curl -s https://smsgrab.com/api/dev/v1/countries \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "items": [ { "id": "indonesia", "name": "Indonesia", "iso_code": "ID", "dial_code": "62", "flag_emoji": "🇮🇩",
"available_numbers": 812334, "service_count": 402, "compat_id": 6 } ] }
GET
/services
Usługi, opcjonalnie tylko dostępne w jednym kraju. uprawnienie: read
| Parameter | In | Type | Description |
|---|---|---|---|
country | query | string | Only services in stock in this country. With it, min_price_minor is your price there and country_count is omitted. |
curl -s "https://smsgrab.com/api/dev/v1/services?country=indonesia" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "items": [ { "id": "whatsapp", "name": "WhatsApp", "category": "MESSAGING",
"icon_url": "https://smsgrab.com/api/v1/icons/whatsapp?v=1fceb4feb3", "compat_code": "wa",
"min_price_minor": 17 } ] }
GET
/prices
Twoje ceny deweloperskie i zapas dla każdej oferty. uprawnienie: read
| Parameter | In | Type | Description |
|---|---|---|---|
service | query | string | A service id. Leave it out for every service. |
country | query | string | A country id. Leave it out for every country. |
curl -s --compressed "https://smsgrab.com/api/dev/v1/prices?service=whatsapp&country=indonesia" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "currency": "USD", "markup_percent": 70,
"items": [ { "service_id": "whatsapp", "country_id": "indonesia", "price_minor": 17, "retail_price_minor": 25,
"available_numbers": 30412 } ] }
POST
/activations
Kup numer. uprawnienie: purchase
| Parameter | In | Type | Description |
|---|---|---|---|
service required | body | string | A service id (whatsapp) or its compatible code (wa). |
country required | body | string | A country id (indonesia), an ISO code (ID) or a compatible id (6). |
max_price_minor | body | integer | The most you want to pay, in cents. A higher price answers 409 PRICE_CHANGED and nothing is charged. |
Idempotency-Key | header | string | Any unique string up to 64 characters. Repeating it replays the first success instead of buying twice. |
curl -s -X POST https://smsgrab.com/api/dev/v1/activations \
-H "Authorization: Bearer $SMSGRAB_API_KEY" \
-H "Idempotency-Key: order-7f3c2a" \
-H "Content-Type: application/json" \
-d '{"service":"whatsapp","country":"indonesia","max_price_minor":20}'
{ "id": "5c1d0c2e-9f3a-4b7e-8a61-2f0c9d4e7b10", "compat_id": 100000123, "status": "WAITING_SMS",
"service_id": "whatsapp", "service_name": "WhatsApp", "country_id": "indonesia", "country_name": "Indonesia",
"country_iso_code": "ID", "phone_number": "+6281234567890", "price_minor": 17, "currency": "USD",
"created_at": "2026-09-27T10:05:00.000Z", "expires_at": "2026-09-27T10:25:00.000Z", "finished_at": null,
"refunded": false, "refund_minor": 0, "sms": [], "api_key_id": "0b8e2a41-7c1d-4e5f-9a3b-6c7d8e9f0a1b" }
GET
/activations
Wyświetl swoje aktywacje. uprawnienie: read
| Parameter | In | Type | Description |
|---|---|---|---|
status | query | string | active (waiting or code received) or finished. |
service | query | string | Only this service id. |
country | query | string | Only this country id. |
from, to | query | date-time | Only activations bought in this time window (ISO 8601). |
source | query | string | api (default, bought through the developer API or the compatible endpoint) or all (the whole account). |
limit | query | integer | 1 to 100, default 50. |
cursor | query | string | next_cursor of the previous page. |
curl -s "https://smsgrab.com/api/dev/v1/activations?status=active&limit=20" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "items": [ { "id": "5c1d0c2e-9f3a-4b7e-8a61-2f0c9d4e7b10", "status": "CODE_RECEIVED", "phone_number": "+6281234567890" } ], "next_cursor": "MTc5NDI1" }
GET
/activations/{id}
Jedna aktywacja, z opcjonalnym long pollingiem. uprawnienie: read
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string | The activation UUID or its numeric compat_id. |
wait | query | integer | 0 to 30 seconds. The request is held until the activation changes (a new SMS or status) or the time runs out. At most 20 held requests per key. |
curl -s "https://smsgrab.com/api/dev/v1/activations/100000123?wait=25" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "id": "5c1d0c2e-9f3a-4b7e-8a61-2f0c9d4e7b10", "compat_id": 100000123, "status": "CODE_RECEIVED",
"phone_number": "+6281234567890", "price_minor": 17, "currency": "USD",
"sms": [ { "code": "482913", "text": "Your WhatsApp code is 482-913", "sender": "WhatsApp",
"received_at": "2026-09-27T10:06:11.870Z" } ] }
POST
/activations/{id}/cancel
Anuluj przed nadejściem kodu i odzyskaj opłatę. uprawnienie: purchase
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string | The activation UUID or compat_id. Accepted only while waiting for the first SMS; the charge is refunded. |
curl -s -X POST https://smsgrab.com/api/dev/v1/activations/100000123/cancel \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "id": "5c1d0c2e-9f3a-4b7e-8a61-2f0c9d4e7b10", "status": "CANCELLED", "refunded": true, "refund_minor": 17 }
POST
/activations/{id}/finish
Zakończ aktywację po otrzymaniu kodu. uprawnienie: purchase
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string | The activation UUID or compat_id. Accepted once a code has arrived. |
curl -s -X POST https://smsgrab.com/api/dev/v1/activations/100000123/finish \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "id": "5c1d0c2e-9f3a-4b7e-8a61-2f0c9d4e7b10", "status": "COMPLETED", "finished_at": "2026-09-27T10:07:02.113Z" }
GET
/usage
Żądania, zakupy i wydatki w czasie. uprawnienie: read
| Parameter | In | Type | Description |
|---|---|---|---|
from, to | query | date | YYYY-MM-DD, at most 366 days apart. Default: the last 30 days. |
granularity | query | string | day (default) or month. |
key | query | string | current (default, the calling key) or all (every key of the account). |
curl -s "https://smsgrab.com/api/dev/v1/usage?granularity=day&key=all" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
{ "currency": "USD", "from": "2026-09-01", "to": "2026-09-27", "granularity": "day",
"totals": { "requests": 48211, "errors": 120, "rate_limited": 3, "purchases": 1804, "spend_minor": 31211,
"refunds_minor": 2410, "net_minor": 28801 },
"series": [ { "date": "2026-09-27", "requests": 2103, "errors": 4, "rate_limited": 0, "purchases": 88,
"spend_minor": 1496, "refunds_minor": 102, "net_minor": 1394 } ],
"by_key": [ { "key_id": "0b8e2a41-7c1d-4e5f-9a3b-6c7d8e9f0a1b", "name": "production bot", "requests": 48211, "net_minor": 28801 } ] }
Cykl życia aktywacji
Aktywacja przechodzi przez kilka stanów od zakupu do kodu.
WAITING_SMSCODE_RECEIVEDCOMPLETED
WAITING_SMS: the number is ready and waiting for the first message. It can be cancelled (refunded).CODE_RECEIVED: at least one SMS arrived;smsholds every message withcode,text,senderandreceived_at. More messages may follow.COMPLETED: you finished it, or the code window ended after a code.CANCELLEDandEXPIRED: no code arrived. The charge is refunded andrefunded/refund_minorare set.
A number lives 20 minutes (expires_at). Cancel is accepted only while waiting (422 ACTIVATION_CODE_RECEIVED after a code; 422 ACTIVATION_CANCEL_NOT_YET when an early cancel is refused, retry after a minute). Finish is accepted once a code has arrived.
Long polling. GET /activations/{id}?wait=25 holds the request until the activation changes or the time runs out, then answers the current object. It replaces tight polling loops and costs one request per change.
Webhooki
Webhooki wysyłają zdarzenia aktywacji na Twój serwer w chwili, gdy się dzieją.
| Event | data |
|---|---|
activation.created | activation |
activation.code_received | activation (with every SMS so far) |
activation.completed | activation |
activation.cancelled | activation |
activation.expired | activation |
activation.refunded | activation |
balance.low | { balance_minor, currency, threshold_minor } |
webhook.test | { message } |
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: SmsGrab-Webhooks/1.0
SmsGrab-Event: activation.code_received
SmsGrab-Delivery: 9f7d6c1e-2b4a-4f0e-9c3d-1a2b3c4d5e6f
SmsGrab-Signature: t=1790503200,v1=3b9a0c5e7d2f41a8b6c9e0d1f2a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5
{
"id": "evt_4e1f9a0b2c3d",
"type": "activation.code_received",
"created_at": "2026-09-27T10:06:12.004Z",
"api_version": "2026-09-27",
"data": {
"activation": {
"id": "5c1d0c2e-9f3a-4b7e-8a61-2f0c9d4e7b10",
"status": "CODE_RECEIVED",
"sms": [{ "code": "482913", "text": "Your WhatsApp code is 482-913", "sender": "WhatsApp", "received_at": "2026-09-27T10:06:11.870Z" }]
}
}
}
- Success is any 2xx answer within 10 seconds. Redirects are never followed.
- Retries after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours, 12 hours and 24 hours (±10 % jitter), then the delivery is marked
dead. A retry repeats the same eventid, so de-duplicate by it. - Order: events of one activation are delivered in order.
- 410 Gone switches the webhook off at once. After 50 failed attempts spanning at least 24 hours it is switched off and you get a notification.
- URLs must be
https://on port 443 or 1024–65535, use a host name, and resolve only to public addresses. Private, loopback, link-local and reserved ranges are refused. - Deliveries are kept 30 days. Send a test event, see every attempt and redeliver from your account.
Sprawdzanie podpisów
Sprawdź każde dostarczenie, zanim mu zaufasz.
SmsGrab-Signature: t=<unix seconds>,v1=<hex>. v1 is the lowercase hex HMAC-SHA256 of "<t>.<raw body>" with your webhook secret (whsec_…, shown once). Verify against the raw bytes before parsing JSON, reject a t more than 300 seconds from your clock, and compare in constant time.
PHP
<?php
function verifySmsGrabSignature(string $header, string $body, string $secret): bool
{
$parts = [];
foreach (explode(',', $header) as $part) {
[$name, $value] = array_pad(explode('=', $part, 2), 2, '');
$parts[trim($name)] = trim($value);
}
$timestamp = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $timestamp.'.'.$body, $secret);
return abs(time() - $timestamp) <= 300 && hash_equals($expected, $parts['v1'] ?? '');
}
$body = file_get_contents('php://input');
if (! verifySmsGrabSignature($_SERVER['HTTP_SMSGRAB_SIGNATURE'] ?? '', $body, getenv('SMSGRAB_WEBHOOK_SECRET'))) {
http_response_code(400);
exit;
}
$event = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
http_response_code(200);
Python
import hashlib
import hmac
import time
def verify(header: str, body: bytes, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
timestamp = int(parts.get("t", "0"))
expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
return abs(time.time() - timestamp) <= 300 and hmac.compare_digest(expected, parts.get("v1", ""))
Node.js
import crypto from 'node:crypto';
export function verify(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=', 2)));
const timestamp = Number(parts.t ?? 0);
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
const given = Buffer.from(parts.v1 ?? '', 'utf8');
return Math.abs(Date.now() / 1000 - timestamp) <= 300
&& given.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), given);
}
Go
package webhooks
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
)
func Verify(header string, body []byte, secret string) bool {
var timestamp int64
var signature string
for _, part := range strings.Split(header, ",") {
name, value, found := strings.Cut(strings.TrimSpace(part), "=")
if !found {
continue
}
switch name {
case "t":
timestamp, _ = strconv.ParseInt(value, 10, 64)
case "v1":
signature = value
}
}
age := time.Now().Unix() - timestamp
if age > 300 || age < -300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(strconv.FormatInt(timestamp, 10) + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
Zgodność z sms-activate
Narzędzia napisane dla protokołu sms-activate działają po zmianie jednego adresu.
https://smsgrab.com/api/stubs/handler_api.php
GET or POST (form encoded) with api_key, action and the action's parameters. The key may also be sent as Authorization: Bearer. Answers are plain text (or JSON where shown) with HTTP 200 for every protocol answer, exactly like the original protocol; rate limiting answers HTTP 429 with TOO_MANY_REQUESTS. Amounts are decimal USD with two decimals (your developer price). A wrong, revoked or expired key answers BAD_KEY; an unknown action answers BAD_ACTION.
| action | Parameters | Success | Errors |
|---|---|---|---|
getBalance | – | ACCESS_BALANCE:12.34 | BAD_KEY |
getNumber | service, country, maxPrice? | ACCESS_NUMBER:100000123:6281234567890 | BAD_SERVICE, BAD_COUNTRY, NO_NUMBERS, NO_BALANCE, WRONG_MAX_PRICE:0.17, CHANNELS_LIMIT, BANNED:…, NO_ACCESS |
getNumberV2 | service, country, maxPrice? | {"activationId":"100000123","phoneNumber":"6281234567890","activationCost":"0.17",…} | same as getNumber |
setStatus | id, status (1, 3, 6, 8) | ACCESS_READY, ACCESS_RETRY_GET, ACCESS_ACTIVATION, ACCESS_CANCEL | NO_ACTIVATION, BAD_STATUS, EARLY_CANCEL_DENIED |
getStatus | id | STATUS_WAIT_CODE, STATUS_WAIT_RETRY:…, STATUS_OK:482913, STATUS_CANCEL | NO_ACTIVATION |
getStatusV2 | id | {"verificationType":0,"sms":{"code":"482913",…},"call":null} | NO_ACTIVATION |
getActiveActivations | – | {"status":"success","activeActivations":[…]} | {"status":"error","error":"NO_ACTIVATIONS"} |
getPrices | service?, country? | {"6":{"wa":{"cost":0.17,"count":30412}}} | BAD_SERVICE, BAD_COUNTRY |
getCountries | – | {"6":{"id":6,"eng":"Indonesia",…}} | – |
getServicesList | country? | {"status":"success","services":[{"code":"wa","name":"WhatsApp"}]} | – |
getNumbersStatus | country? | {"wa_0":"30412","tg_0":"1822"} | BAD_COUNTRY |
getTopCountriesByService | service | {"0":{"country":6,"count":30412,"price":0.17}} | BAD_SERVICE |
setStatus: 1 ready, 3 wait for another SMS, 6 finish, 8 cancel and refund. getActiveActivations reports 4 while waiting and 1 after a code. Times are UTC, yyyy-MM-dd HH:mm:ss. The activation id is the numeric compat_id, which the REST API accepts too.
Service codes
waWhatsApptgTelegramviViberigInstagramfbFacebookgoGoogletwX / TwitterdsDiscordlfTikTokamAmazonmmMicrosoftmbYahoooiTinderubUbertsPayPalnfNetflixwbWeChatvkVKontakteokOdnoklassnikidheBaykaShopeefuSnapchatmtSteambwSignaldrOpenAImeLINEktKakaoTalkotAny other
Country ids
0Russia6Indonesia4Philippines10Vietnam16United Kingdom22India36Canada43Germany73Brazil187USA
Our catalogue ids and ISO country codes are accepted as well, so every product is reachable. getCountries and getServicesList are the authoritative lists. Keys, scopes, allowlists, spend caps, limits, prices, refunds, webhooks and metering are the same as for the REST API.
curl
BASE="https://smsgrab.com/api/stubs/handler_api.php?api_key=$SMSGRAB_API_KEY"
curl -s "$BASE&action=getBalance"
curl -s "$BASE&action=getNumber&service=tg&country=6"
curl -s "$BASE&action=getStatus&id=100000123"
curl -s "$BASE&action=setStatus&id=100000123&status=6"
Python
import os
import requests
BASE = "https://smsgrab.com/api/stubs/handler_api.php"
KEY = os.environ["SMSGRAB_API_KEY"]
def action(name: str, **params) -> str:
response = requests.get(BASE, params={"api_key": KEY, "action": name, **params}, timeout=30)
response.raise_for_status()
return response.text
print(action("getBalance"))
answer = action("getNumber", service="tg", country=6)
if answer.startswith("ACCESS_NUMBER:"):
_, activation_id, number = answer.split(":")
print(activation_id, number)
Pełne przykłady
Kup numer, poczekaj na kod i zakończ aktywację – w pięciu językach.
curl
export SMSGRAB_API_KEY="sgk_your_key"
curl -s https://smsgrab.com/api/dev/v1/balance \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
curl -s -X POST https://smsgrab.com/api/dev/v1/activations \
-H "Authorization: Bearer $SMSGRAB_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"service":"telegram","country":"indonesia","max_price_minor":30}'
curl -s "https://smsgrab.com/api/dev/v1/activations/100000123?wait=25" \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
curl -s -X POST https://smsgrab.com/api/dev/v1/activations/100000123/finish \
-H "Authorization: Bearer $SMSGRAB_API_KEY"
Python
import os
import uuid
import requests
API = "https://smsgrab.com/api/dev/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SMSGRAB_API_KEY']}"
def buy(service: str, country: str, max_price_minor: int) -> dict:
response = session.post(
f"{API}/activations",
json={"service": service, "country": country, "max_price_minor": max_price_minor},
headers={"Idempotency-Key": str(uuid.uuid4())},
timeout=30,
)
response.raise_for_status()
return response.json()
def wait_for_code(activation_id: str) -> str | None:
while True:
response = session.get(f"{API}/activations/{activation_id}", params={"wait": 25}, timeout=40)
response.raise_for_status()
activation = response.json()
if activation["sms"]:
return activation["sms"][-1]["code"]
if activation["status"] != "WAITING_SMS":
return None
activation = buy("telegram", "indonesia", 30)
print("Number:", activation["phone_number"])
code = wait_for_code(activation["id"])
if code:
print("Code:", code)
session.post(f"{API}/activations/{activation['id']}/finish", timeout=30).raise_for_status()
else:
print("No code arrived, the charge was refunded")
Node.js
import { randomUUID } from 'node:crypto';
const API = 'https://smsgrab.com/api/dev/v1';
async function call(path, { method = 'GET', body, idempotencyKey } = {}) {
const response = await fetch(`${API}${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.SMSGRAB_API_KEY}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = response.status === 204 ? null : await response.json();
if (!response.ok) {
throw new Error(`${data?.code ?? response.status}: ${data?.message ?? ''}`);
}
return data;
}
let activation = await call('/activations', {
method: 'POST',
body: { service: 'telegram', country: 'indonesia', max_price_minor: 30 },
idempotencyKey: randomUUID(),
});
console.log('Number:', activation.phone_number);
while (activation.sms.length === 0 && activation.status === 'WAITING_SMS') {
activation = await call(`/activations/${activation.id}?wait=25`);
}
if (activation.sms.length > 0) {
console.log('Code:', activation.sms.at(-1).code);
await call(`/activations/${activation.id}/finish`, { method: 'POST' });
} else {
console.log('No code arrived, the charge was refunded');
}
PHP
<?php
require __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Client;
$client = new Client([
'base_uri' => 'https://smsgrab.com/api/dev/v1/',
'headers' => ['Authorization' => 'Bearer '.getenv('SMSGRAB_API_KEY')],
'timeout' => 40,
]);
$read = static fn ($response): array => json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
$activation = $read($client->post('activations', [
'json' => ['service' => 'telegram', 'country' => 'indonesia', 'max_price_minor' => 30],
'headers' => ['Idempotency-Key' => bin2hex(random_bytes(16))],
]));
echo 'Number: '.$activation['phone_number'].PHP_EOL;
while ($activation['sms'] === [] && $activation['status'] === 'WAITING_SMS') {
$activation = $read($client->get('activations/'.$activation['id'], ['query' => ['wait' => 25]]));
}
if ($activation['sms'] !== []) {
echo 'Code: '.$activation['sms'][array_key_last($activation['sms'])]['code'].PHP_EOL;
$client->post('activations/'.$activation['id'].'/finish');
} else {
echo 'No code arrived, the charge was refunded'.PHP_EOL;
}
Go
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
const api = "https://smsgrab.com/api/dev/v1"
type Activation struct {
ID string `json:"id"`
Status string `json:"status"`
PhoneNumber string `json:"phone_number"`
SMS []struct {
Code string `json:"code"`
} `json:"sms"`
}
var client = &http.Client{Timeout: 40 * time.Second}
func call(method, path string, body any, idempotencyKey string, out any) error {
var payload bytes.Buffer
if body != nil {
if err := json.NewEncoder(&payload).Encode(body); err != nil {
return err
}
}
request, err := http.NewRequest(method, api+path, &payload)
if err != nil {
return err
}
request.Header.Set("Authorization", "Bearer "+os.Getenv("SMSGRAB_API_KEY"))
if body != nil {
request.Header.Set("Content-Type", "application/json")
}
if idempotencyKey != "" {
request.Header.Set("Idempotency-Key", idempotencyKey)
}
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode >= 300 {
var failure struct {
Code string `json:"code"`
Message string `json:"message"`
}
_ = json.NewDecoder(response.Body).Decode(&failure)
return fmt.Errorf("%s: %s", failure.Code, failure.Message)
}
if out == nil {
return nil
}
return json.NewDecoder(response.Body).Decode(out)
}
func idempotencyKey() string {
buffer := make([]byte, 16)
_, _ = rand.Read(buffer)
return hex.EncodeToString(buffer)
}
func main() {
var activation Activation
order := map[string]any{"service": "telegram", "country": "indonesia", "max_price_minor": 30}
if err := call("POST", "/activations", order, idempotencyKey(), &activation); err != nil {
panic(err)
}
fmt.Println("Number:", activation.PhoneNumber)
for len(activation.SMS) == 0 && activation.Status == "WAITING_SMS" {
if err := call("GET", "/activations/"+activation.ID+"?wait=25", nil, "", &activation); err != nil {
panic(err)
}
}
if len(activation.SMS) == 0 {
fmt.Println("No code arrived, the charge was refunded")
return
}
fmt.Println("Code:", activation.SMS[len(activation.SMS)-1].Code)
if err := call("POST", "/activations/"+activation.ID+"/finish", nil, "", nil); err != nil {
panic(err)
}
}
Wersje i zmiany
Dbamy o stabilność API i informujemy o zmianach.
- The current version is
2026-09-27; webhook payloads carry it inapi_version. - We add fields, events and error reasons without notice. Ignore fields and reasons you do not know.
- Breaking changes get a new path (
/dev/v2) and a long overlap. - The machine-readable description is part of the OpenAPI document at
https://smsgrab.com/api/v1/openapi.json.