Automatizar verificações por SMS é uma necessidade crescente para equipes de desenvolvimento, QA e operações que escalam rapidamente. A API REST do SMSCode resolve isso — você integra uma vez e consegue números virtuais e códigos SMS programaticamente em qualquer linguagem, em qualquer escala.
Se você trabalha com desenvolvimento de software, automação de testes ou fluxos que precisam receber SMS programaticamente, a API elimina etapas manuais e mantém cada pedido rastreável pelo identificador retornado.
Este guia cobre tudo: pré-requisitos, fluxo completo com exemplos de código em 4 linguagens, tratamento de erros, boas práticas de integração e casos de uso avançados.
TL;DR: A API do SMSCode usa Bearer Token, retorna JSON e tem fluxo de 4 etapas: consultar catálogo → criar pedido (a atribuição do número pode continuar pendente) → fazer polling do SMS → cancelar somente quando
can_cancel: true. Base URL:https://api.smscode.gg/v1. Token em Configurações > API.
Pré-requisitos para usar a API
Antes de escrever uma linha de código:
- Conta ativa no SMSCode — Crie em smscode.gg com apenas um e-mail, sem documentos
- Saldo de crédito — Adicione por um método exibido na página Deposit da sua conta; opções e condições podem mudar
- Token de API — Disponível em Configurações > API do painel (64 caracteres hexadecimais)
- Ferramenta para chamadas HTTP — cURL para testes, ou cliente HTTP na sua linguagem preferida
Como encontrar o token de API
- Faça login em smscode.gg
- Clique no seu avatar ou e-mail no canto superior direito
- Vá em “Settings” > seção “API”
- Clique para revelar e copie o token
O token tem 64 caracteres hexadecimais. Trate-o como senha: não commite em repositórios públicos, não exponha em variáveis de ambiente do lado cliente, não cole em mensagens de chat. Se suspeitar de comprometimento, revogue e gere um novo na mesma tela.
# Exemplo de token (não é real — apenas para ilustrar o formato)
SMSCODE_TOKEN=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Sempre carregue o token de variável de ambiente, nunca hardcoded no código:
export SMSCODE_TOKEN="seu_token_aqui"
Visão geral da arquitetura da API
A API do SMSCode segue o padrão REST com algumas características importantes:
- Autenticação: Bearer Token via header
Authorization - Formato de dados: JSON para requests e responses
- Base URL:
https://api.smscode.gg/v1 - Erros: HTTP status codes + JSON com código de erro descritivo
- Rate limiting: ao receber
429, respeite umRetry-Afterpositivo ou use um fallback limitado
O fluxo completo de uma verificação via API tem quatro etapas:
1. GET /v1/catalog/products → Verificar disponibilidade e preço
2. POST /v1/orders/create → Criar pedido (obtém ID; `phone_number` pode ser nulo)
3. GET /v1/orders/{id} → Polling de status (ler SMS quando chegar)
4. POST /v1/orders/cancel → Cancelar somente quando `can_cancel` permitir
O {id} é o identificador único do pedido retornado no passo 2. As etapas 1 e 4 são opcionais — mas recomendadas para integração robusta.
Autenticação
Todas as chamadas autenticadas usam o header Authorization:
Authorization: Bearer SEU_TOKEN_AQUI
Content-Type: application/json
Sem o header de autorização: API retorna 401 Unauthorized.
Com token válido mas sem saldo suficiente: 409 Conflict.
Teste de autenticação rápido:
curl -X GET "https://api.smscode.gg/v1/balance" \
-H "Authorization: Bearer $SMSCODE_TOKEN"
Resposta esperada:
{
"success": true,
"data": {
"currency": "IDR",
"balance": 425000
}
}
Se receber 200 com success: true, o token está correto e funcionando.
Etapa 1: Consultar o catálogo
Antes de criar um pedido, verifique serviços disponíveis e preços:
# Verificar produtos de uma plataforma e um país
curl -X GET "https://api.smscode.gg/v1/catalog/products?country_id=7&platform_id=1" \
-H "Authorization: Bearer $SMSCODE_TOKEN"
Resposta:
{
"success": true,
"data": [
{
"id": 1024,
"name": "WhatsApp - Indonesia",
"catalog_product_id": 88,
"country_id": 7,
"platform_id": 1,
"available": 142,
"price": 18000,
"active": true
}
],
"meta": { "page": 1, "limit": 1000, "count": 1 }
}
Campos importantes:
price: preço inteiro em IDRavailable: quantidade aproximada disponívelactive: indica se o produto está ativocatalog_product_id: identificador usado na criação do pedidocountry_ideplatform_id: filtros do catálogo
/v1/catalog/products é paginado. Percorra meta.page, meta.limit e meta.count até consumir as páginas necessárias; em integrações, filtre por country_id e platform_id e guarde o catalog_product_id escolhido antes da compra.
Códigos de serviço mais usados:
| Serviço | Código |
|---|---|
wa |
|
| Telegram | tg |
ig |
|
| Google/Gmail | go |
fb |
|
| TikTok | tt |
| Twitter/X | tw |
| Discord | di |
| Amazon | amazon |
| Binance | bn |
Códigos de país comuns:
| País | Código |
|---|---|
| Brasil | br |
| Estados Unidos | us |
| Índia | in |
| Reino Unido | gb |
| Alemanha | de |
| Portugal | pt |
Etapa 2: Criar pedido (solicitar número virtual)
curl --connect-timeout 5 --max-time 30 -X POST "https://api.smscode.gg/v1/orders/create" \
-H "Authorization: Bearer $SMSCODE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-example-001" \
-d '{
"catalog_product_id": 88,
"quantity": 1
}'
Resposta bem-sucedida:
{
"success": true,
"data": {
"orders": [{
"id": 90210,
"status": "ACTIVE",
"phone_number": "+919876543210",
"otp_code": null,
"otp_received_at": null,
"expires_at": "2026-03-17T14:25:00Z",
"failed_reason": null,
"product_id": 1024,
"catalog_product_id": 88,
"operator_id": null,
"operator_name": null,
"amount": 750000,
"can_finish": false,
"can_resend": false,
"can_cancel": false,
"can_replace": false,
"can_reactivate": false,
"resend_available_at": null,
"cancel_available_at": "2026-03-17T14:02:00Z",
"replace_available_at": "2026-03-17T14:02:00Z"
}],
"failed_count": 0
}
}
Campos importantes:
data.orders[0].id: salve imediatamente para todas as operações subsequentesphone_number: número virtual completo com DDI quando já atribuído; é opcional/anulável até a atribuição terminarstatus: começa comoACTIVEexpires_at: prazo definido pelo servidor para aquele pedido; não fixe uma duração típica no cliente
Após uma criação resolvida, use phone_number somente se for uma string não vazia. O fluxo durável completo faz no máximo uma leitura limitada de GET /orders/{id} para o mesmo pedido; se o campo continuar ausente, nulo ou em branco, mantém o resultado explícito pending_assignment em vez de iniciar o uso do número. Se o resultado do POST for ambíguo, repita somente com o mesmo Idempotency-Key e exatamente o mesmo JSON body; nunca troque produto, key ou body antes de reconciliar a tentativa.
Etapa 3: Polling de status e leitura do SMS
Após usar o número na plataforma e solicitar o código, faça polling periódico:
curl --connect-timeout 5 --max-time 30 -X GET "https://api.smscode.gg/v1/orders/90210" \
-H "Authorization: Bearer $SMSCODE_TOKEN"
Projeção de V1OrderSummary enquanto aguarda (campos selecionados, não a resposta wire completa):
{
"success": true,
"data": {
"id": 90210,
"status": "ACTIVE",
"otp_code": null,
"otp_message": null,
"sms_revision": 0,
"can_cancel": true
}
}
Projeção de V1OrderSummary após o SMS (campos selecionados, não a resposta wire completa):
{
"success": true,
"data": {
"id": 90210,
"status": "OTP_RECEIVED",
"otp_code": null,
"otp_message": "Seu código do WhatsApp é 483920. Não compartilhe com ninguém.",
"sms_revision": 1,
"can_cancel": false
}
}
Valores do campo status:
ACTIVE: aguardando SMSOTP_RECEIVED: mensagem recebida;otp_codepode sernullCOMPLETED: pedido concluído após a entregaEXPIRED: janela do pedido encerradaCANCELED: pedido cancelado
Mantenha por pedido uma revisão aceita, iniciada em -1. Em cada snapshot, leia sms_revision e otp_message antes de avaliar o ciclo de vida: aceite somente uma revisão inteira (não booleana), estritamente maior que a revisão aceita, acompanhada de uma mensagem string não vazia. Atualize a revisão e entregue a mensagem antes dos ramos terminais, independentemente de status e de otp_code; evidência inválida, antiga ou em branco não avança a revisão. Trate COMPLETED, EXPIRED e CANCELED como estados terminais depois dessa etapa.
Defina um intervalo de polling e um timeout total limitados pela sua aplicação. Ao receber 429,
respeite um Retry-After positivo; se ele estiver ausente ou inválido, use um atraso de fallback
limitado. Não trate nenhum intervalo local como cota garantida pela API.
Etapa 4: Cancelar pedido
Se sua lógica não precisa mais do pedido, consulte primeiro o snapshot atual. Envie o cancelamento somente se can_cancel for true:
curl --connect-timeout 5 --max-time 30 -X POST "https://api.smscode.gg/v1/orders/cancel" \
-H "Authorization: Bearer $SMSCODE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": 90210}'
A resposta do servidor e a política do pedido são a autoridade para qualquer efeito no saldo; não presuma cancelabilidade ou reembolso apenas pelo estado local do cliente.
Verificar saldo via API
Útil para alertas de saldo baixo em sistemas automatizados:
curl -X GET "https://api.smscode.gg/v1/balance" \
-H "Authorization: Bearer $SMSCODE_TOKEN"
Retorna o saldo atual como inteiro em IDR. Implemente a checagem antes de iniciar operações em lote para evitar falhas no meio do processo.
Exemplos completos por linguagem
Python — classe completa de integração
import os
import time
import requests
import uuid
from typing import Optional
API_TOKEN = os.environ["SMSCODE_TOKEN"]
BASE_URL = "https://api.smscode.gg/v1"
ORDER_TIMEOUT = (5, 30)
HEADERS = {
"Authorization": f"Bearer {API_TOKEN}",
"Content-Type": "application/json"
}
class SMSCodeClient:
"""Cliente para a API do SMSCode."""
def check_availability(self, country_id: int, platform_id: int) -> dict:
"""Verifica disponibilidade e preço antes de criar pedido."""
response = requests.get(
f"{BASE_URL}/catalog/products",
headers=HEADERS,
params={"country_id": country_id, "platform_id": platform_id}
)
response.raise_for_status()
data = response.json()["data"]
if not data:
raise ValueError("Produto não encontrado no catálogo")
return data[0]
def create_order(self, catalog_product_id: int) -> dict:
"""Cria pedido e retorna número virtual + ID do pedido."""
body = {"catalog_product_id": catalog_product_id, "quantity": 1}
idempotency_key = str(uuid.uuid4())
response = requests.post(
f"{BASE_URL}/orders/create",
headers={**HEADERS, "Idempotency-Key": idempotency_key},
json=body,
timeout=ORDER_TIMEOUT,
)
response.raise_for_status()
return response.json()["data"]["orders"][0]
def get_order_status(self, order_id: int) -> dict:
"""Consulta status do pedido."""
response = requests.get(
f"{BASE_URL}/orders/{order_id}",
headers=HEADERS,
timeout=ORDER_TIMEOUT,
)
response.raise_for_status()
return response.json()["data"]
def cancel_order(self, order_id: int) -> None:
"""Cancela somente quando o snapshot atual permite."""
current = self.get_order_status(order_id)
if current["can_cancel"]:
requests.post(
f"{BASE_URL}/orders/cancel",
headers=HEADERS,
json={"id": order_id},
timeout=ORDER_TIMEOUT,
).raise_for_status()
def get_sms_code(
self,
catalog_product_id: int,
timeout_seconds: int = 300,
poll_interval: int = 5
) -> dict:
"""
Fluxo completo: cria pedido, aguarda SMS e retorna resultado.
Returns:
dict com 'phone', 'code' e 'text'
Raises:
TimeoutError: SMS não chegou no prazo
ValueError: Pedido cancelado ou expirado
"""
# Criar pedido
order = self.create_order(catalog_product_id)
order_id = order["id"]
# `phone_number` é opcional/anulável até a atribuição. O create resolvido
# continua resolvido: uma única leitura limitada com o mesmo id, nunca
# outro create pago.
phone = order.get("phone_number")
if not (isinstance(phone, str) and phone.strip()):
phone = self.get_order_status(order_id).get("phone_number")
if not (isinstance(phone, str) and phone.strip()):
# pending_assignment: o pedido segue resolvido e cobrado; não use o
# número em lugar nenhum e não crie outro pedido.
return {"kind": "pending_assignment", "order_id": order_id}
print(f"Número obtido: {phone} | Pedido: {order_id}")
# Polling com backoff exponencial
start_time = time.time()
delay = poll_interval
last_seen_revision = -1
try:
while time.time() - start_time < timeout_seconds:
time.sleep(delay)
status = self.get_order_status(order_id)
revision = status.get("sms_revision")
message = status.get("otp_message")
if (
type(revision) is int
and revision > last_seen_revision
and isinstance(message, str)
and message.strip()
):
last_seen_revision = revision
return {
"phone": phone,
"code": status.get("otp_code"),
"text": message,
"revision": revision
}
lifecycle_status = status["status"]
if lifecycle_status in ["COMPLETED", "EXPIRED", "CANCELED"]:
raise ValueError(f"Pedido finalizado: {lifecycle_status}")
# ACTIVE e OTP_RECEIVED continuam em polling sem nova mensagem válida.
# Backoff leve: aumenta o intervalo até 15s máximo
delay = min(delay * 1.3, 15)
except (TimeoutError, ValueError):
self.cancel_order(order_id)
raise
self.cancel_order(order_id)
raise TimeoutError(f"SMS não chegou em {timeout_seconds}s")
# Exemplo de uso
client = SMSCodeClient()
try:
result = client.get_sms_code(88)
if result.get("kind") == "pending_assignment":
# O pedido segue resolvido e cobrado; não use o número nem crie outro.
print(f"pending_assignment: pedido {result['order_id']} ainda sem número")
else:
print(f"Número: {result['phone']}")
print(f"Código: {result['code']}")
print(f"Texto do SMS: {result['text']}")
except TimeoutError:
print("Timeout — tente com outro país")
except ValueError as e:
print(f"Erro: {e}")
Node.js (TypeScript) — com tipos e tratamento de erros
import { env } from "process";
import { Agent, fetch } from "undici";
const API_TOKEN = env.SMSCODE_TOKEN!;
const BASE_URL = "https://api.smscode.gg/v1";
const orderDispatcher = new Agent({ connectTimeout: 5_000 });
const orderTransport = () => ({
dispatcher: orderDispatcher,
signal: AbortSignal.timeout(30_000)
});
const headers = {
Authorization: `Bearer ${API_TOKEN}`,
"Content-Type": "application/json",
};
type OrderState = "ACTIVE" | "OTP_RECEIVED" | "COMPLETED" | "EXPIRED" | "CANCELED";
interface CreatedOrderView {
id: number;
phone_number: string | null;
status: OrderState;
expires_at: string | null;
}
interface OrderStatusView {
id: number;
status: OrderState;
otp_code: string | null;
otp_message: string | null;
sms_revision: number;
can_cancel: boolean;
}
interface SmsResult {
phone: string;
code: string;
text: string;
}
// `phone_number` é opcional/anulável até a atribuição; o pedido resolvido continua
// resolvido, então o resultado pendente é um estado local distinto — nunca um
// status de pedido da API.
interface PendingAssignment {
kind: "pending_assignment";
orderId: number;
}
type GetSmsOutcome = SmsResult | PendingAssignment;
async function apiRequest<T>(
method: string,
path: string,
body?: object,
extraHeaders: Record<string, string> = {}
): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers: { ...headers, ...extraHeaders },
body: body ? JSON.stringify(body) : undefined,
...orderTransport(),
});
const json = await res.json() as {
success: boolean;
data?: T;
error?: { message?: string };
};
if (!res.ok || !json.success) {
throw new Error(
json.error?.message ?? `HTTP ${res.status}: ${path}`
);
}
if (json.data === undefined) {
throw new Error(`Resposta sem data: ${path}`);
}
return json.data;
}
async function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getSmsCode(
catalogProductId: number,
timeoutMs = 300_000
): Promise<GetSmsOutcome> {
// Criar pedido
const body = { catalog_product_id: catalogProductId, quantity: 1 };
const idempotencyKey = crypto.randomUUID();
const created = await apiRequest<{ orders: CreatedOrderView[] }>(
"POST",
"/orders/create",
body,
{ "Idempotency-Key": idempotencyKey }
);
const order = created.orders[0];
// `phone_number` é opcional/anulável até a atribuição. O create resolvido
// continua resolvido: uma única leitura limitada com o mesmo id, nunca outro
// create pago. Um throw por truthiness aceitaria " " e descartaria o pedido.
const atribuido = (v: unknown): v is string =>
typeof v === "string" && v.trim().length > 0;
let phone = order.phone_number;
if (!atribuido(phone)) {
const atual = await apiRequest<CreatedOrderView>("GET", `/orders/${order.id}`);
phone = atual.phone_number;
}
if (!atribuido(phone)) {
return { kind: "pending_assignment", orderId: order.id } as const;
}
order.phone_number = phone;
console.log(`Número: ${phone} | Pedido: ${order.id}`);
const deadline = Date.now() + timeoutMs;
let delay = 5000;
let lastSeenRevision = -1;
try {
while (Date.now() < deadline) {
await sleep(delay);
const status = await apiRequest<OrderStatusView>(
"GET",
`/orders/${order.id}`
);
const smsRevision = status.sms_revision;
const otpMessage = status.otp_message;
if (
Number.isInteger(smsRevision) &&
smsRevision > lastSeenRevision &&
typeof otpMessage === "string" &&
otpMessage.trim().length > 0
) {
lastSeenRevision = smsRevision;
return {
phone: order.phone_number,
code: status.otp_code ?? "",
text: otpMessage,
};
}
const lifecycleStatus = status.status;
if (["COMPLETED", "EXPIRED", "CANCELED"].includes(lifecycleStatus)) {
throw new Error(`Pedido finalizado: ${lifecycleStatus}`);
}
// Backoff leve
delay = Math.min(delay * 1.3, 15_000);
}
} catch (error) {
const current = await apiRequest<OrderStatusView>("GET", `/orders/${order.id}`);
if (current.can_cancel) {
await apiRequest("POST", "/orders/cancel", { id: order.id });
}
throw error;
}
const current = await apiRequest<OrderStatusView>("GET", `/orders/${order.id}`);
if (current.can_cancel) {
await apiRequest("POST", "/orders/cancel", { id: order.id });
}
throw new Error("Timeout aguardando SMS");
}
// Exemplo de uso
getSmsCode(88)
.then((outcome) => {
if ("kind" in outcome) {
// pending_assignment: o pedido segue resolvido e cobrado; não use o número
// em lugar nenhum e não crie outro pedido.
console.log(`pending_assignment: pedido ${outcome.orderId} ainda sem número`);
return;
}
const { phone, code, text } = outcome;
console.log(`Número: ${phone}`);
console.log(`Código: ${code}`);
console.log(`SMS: ${text}`);
})
.catch((err) => {
console.error("Erro:", err.message);
process.exit(1);
});
PHP — implementação limpa com cURL
<?php
declare(strict_types=1);
class SMSCodeClient
{
private string $token;
private string $baseUrl = 'https://api.smscode.gg/v1';
public function __construct(string $token)
{
$this->token = $token;
}
private function request(
string $method,
string $path,
?array $body = null,
array $extraHeaders = []
): array
{
$ch = curl_init($this->baseUrl . $path);
$headers = [
"Authorization: Bearer {$this->token}",
"Content-Type: application/json",
];
$headers = array_merge($headers, $extraHeaders);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($result === false) {
throw new \RuntimeException("Erro de rede na chamada da API");
}
$data = json_decode($result, true);
if (!$data['success']) {
throw new \RuntimeException(
$data['error']['message'] ?? "Erro HTTP {$httpCode}"
);
}
return $data['data'];
}
public function createOrder(int $catalogProductId): array
{
$body = ['catalog_product_id' => $catalogProductId, 'quantity' => 1];
$key = bin2hex(random_bytes(16));
$data = $this->request(
'POST',
'/orders/create',
$body,
['Idempotency-Key: ' . $key]
);
return $data['orders'][0];
}
public function getOrderStatus(int $orderId): array
{
return $this->request('GET', "/orders/{$orderId}");
}
public function cancelOrder(int $orderId): ?array
{
$current = $this->getOrderStatus($orderId);
if (!$current['can_cancel']) return null;
$result = $this->request('POST', '/orders/cancel', ['id' => $orderId]);
if ($result['status'] !== 'CANCELED') {
throw new \RuntimeException('Cancelamento não confirmado pelo servidor');
}
return $result;
}
public function getSmsCode(
int $catalogProductId,
int $timeoutSeconds = 300,
int $pollInterval = 5
): array {
$order = $this->createOrder($catalogProductId);
$orderId = $order['id'];
// `phone_number` é opcional/anulável até a atribuição. Uma única leitura
// limitada com o mesmo id; nunca outro create pago.
$phone = $order['phone_number'] ?? null;
if (!(is_string($phone) && trim($phone) !== '')) {
// pending_assignment: pedido resolvido e cobrado; não use nem recrie.
return ['kind' => 'pending_assignment', 'order_id' => $orderId];
}
echo "Número: {$phone} | Pedido: {$orderId}\n";
$startTime = time();
$delay = $pollInterval;
$lastSeenRevision = -1;
try {
while ((time() - $startTime) < $timeoutSeconds) {
sleep($delay);
$status = $this->getOrderStatus($orderId);
$smsRevision = $status['sms_revision'] ?? null;
$otpMessage = $status['otp_message'] ?? null;
if (
is_int($smsRevision) &&
$smsRevision > $lastSeenRevision &&
is_string($otpMessage) &&
trim($otpMessage) !== ''
) {
$lastSeenRevision = $smsRevision;
return [
'phone' => $phone,
'code' => $status['otp_code'] ?? null,
'text' => $otpMessage,
];
}
$lifecycleStatus = $status['status'];
if (in_array($lifecycleStatus, ['COMPLETED', 'EXPIRED', 'CANCELED'], true)) {
throw new \RuntimeException("Pedido finalizado: {$lifecycleStatus}");
}
// ACTIVE e OTP_RECEIVED continuam em polling sem nova mensagem válida.
$delay = (int) min($delay * 1.3, 15);
}
} catch (\Exception $e) {
$this->cancelOrder($orderId);
throw $e;
}
$this->cancelOrder($orderId);
throw new \RuntimeException("Timeout: SMS não chegou em {$timeoutSeconds}s");
}
}
// Uso
$client = new SMSCodeClient(getenv('SMSCODE_TOKEN'));
try {
$result = $client->getSmsCode(88);
if (($result['kind'] ?? null) === 'pending_assignment') {
// Pedido resolvido e cobrado; não use o número nem crie outro.
echo "pending_assignment: pedido {$result['order_id']} ainda sem número\n";
} else {
echo "Número: {$result['phone']}\n";
echo "Código: {$result['code']}\n";
echo "SMS: {$result['text']}\n";
}
} catch (\RuntimeException $e) {
echo "Erro: {$e->getMessage()}\n";
exit(1);
}
cURL — fluxo rápido para testes no terminal
#!/bin/bash
set -e
TOKEN="${SMSCODE_TOKEN}"
BASE="https://api.smscode.gg/v1"
CATALOG_PRODUCT_ID=88
IDEMPOTENCY_KEY=$(python3 -c 'import uuid; print(uuid.uuid4())')
echo "1. Criando pedido..."
RESPONSE=$(curl -s --connect-timeout 5 --max-time 30 -X POST "$BASE/orders/create" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d "{\"catalog_product_id\":$CATALOG_PRODUCT_ID,\"quantity\":1}")
ORDER_ID=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['orders'][0]['id'])")
# `phone_number` é opcional/anulável até a atribuição: uma única leitura limitada
# com o mesmo id, nunca outro create pago.
# `.strip()` descarta uma atribuição só com espaços: a OpenAPI não garante
# `minLength`, então "não vazio" precisa ser testado sobre o valor sem espaços.
PHONE=$(echo "$RESPONSE" | python3 -c "import sys,json; print((json.load(sys.stdin)['data']['orders'][0]['phone_number'] or '').strip())")
if [ -z "$PHONE" ]; then
PHONE=$(curl -s --connect-timeout 5 --max-time 30 "$BASE/orders/$ORDER_ID" \
-H "Authorization: Bearer $TOKEN" \
| python3 -c "import sys,json; print((json.load(sys.stdin)['data']['phone_number'] or '').strip())")
fi
if [ -z "$PHONE" ]; then
echo "pending_assignment: o pedido $ORDER_ID ainda não tem número"
exit 0
fi
echo "Número: $PHONE"
echo "Pedido ID: $ORDER_ID"
echo ""
echo "2. Use o número na plataforma agora. Aguardando SMS..."
# Polling
LAST_SEEN_REVISION=-1
# ACTIVE e OTP_RECEIVED não condicionam a aceitação de uma mensagem nova.
for i in $(seq 1 60); do
sleep 5
STATUS_RESPONSE=$(curl -s --connect-timeout 5 --max-time 30 -X GET "$BASE/orders/$ORDER_ID" \
-H "Authorization: Bearer $TOKEN")
SMS_REVISION=$(echo "$STATUS_RESPONSE" | python3 -c "import sys,json; value=json.load(sys.stdin)['data'].get('sms_revision'); print(value if type(value) is int else '')")
SMS_MESSAGE=$(echo "$STATUS_RESPONSE" | python3 -c "import sys,json; value=json.load(sys.stdin)['data'].get('otp_message'); print(value if isinstance(value, str) else '')")
if [[ "$SMS_REVISION" =~ ^[0-9]+$ ]] \
&& [ "$SMS_REVISION" -gt "$LAST_SEEN_REVISION" ] \
&& [ -n "${SMS_MESSAGE//[[:space:]]/}" ]; then
LAST_SEEN_REVISION=$SMS_REVISION
echo "SMS recebido: $SMS_MESSAGE"
exit 0
fi
CURRENT_STATUS=$(echo "$STATUS_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['status'])")
if [ "$CURRENT_STATUS" = "COMPLETED" ] || [ "$CURRENT_STATUS" = "EXPIRED" ] || [ "$CURRENT_STATUS" = "CANCELED" ]; then
echo "Pedido finalizado: $CURRENT_STATUS"
exit 1
fi
echo "Aguardando... ($((i*5))s)"
done
echo "Timeout — verificando elegibilidade de cancelamento..."
CURRENT=$(curl -s --connect-timeout 5 --max-time 30 -X GET "$BASE/orders/$ORDER_ID" -H "Authorization: Bearer $TOKEN")
CAN_CANCEL=$(echo "$CURRENT" | python3 -c "import sys,json; print(str(json.load(sys.stdin)['data']['can_cancel']).lower())")
if [ "$CAN_CANCEL" = "true" ]; then
curl -s --connect-timeout 5 --max-time 30 -X POST "$BASE/orders/cancel" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"id\":$ORDER_ID}" > /dev/null
fi
exit 1
Tratamento de erros
A API retorna erros no formato padrão:
{
"success": false,
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Saldo insuficiente para criar o pedido"
}
}
Tabela de erros e ações recomendadas:
| HTTP | Código de erro | Causa | Ação recomendada |
|---|---|---|---|
| 401 | UNAUTHORIZED |
Token inválido ou ausente | Verifique o token nas configurações |
| 409 | INSUFFICIENT_BALANCE |
Saldo insuficiente | Adicione crédito e tente novamente |
| 422 | NO_OFFER_AVAILABLE |
Nenhum produto atende à seleção | Encerre esta compra; outra seleção é uma nova ação explícita |
| 404 | NOT_FOUND |
ID de pedido inválido | Verifique o ID retornado no passo 2 |
| 422 | VALIDATION_ERROR |
Código de serviço inválido | Consulte o catálogo para códigos válidos |
| 422 | VALIDATION_ERROR |
Código de país inválido | Use ISO 3166-1 alpha-2 |
| 429 | RATE_LIMIT_EXCEEDED |
Muitas requests | Aguarde um Retry-After positivo ou fallback limitado |
| 503 | SERVICE_UNAVAILABLE |
Indisponibilidade temporária | Em create pago, pare e reconcilie antes de outra compra |
Em criação paga, somente NO_OFFER_AVAILABLE, VALIDATION_ERROR, PROVIDER_ERROR e IDEMPOTENCY_KEY_REUSED são falhas definitivas nesta integração. INSUFFICIENT_BALANCE encerra após um POST. Para REQUEST_IN_PROGRESS, repita de forma limitada a mesma key e o mesmo body. Qualquer código desconhecido, JSON malformado ou resultado ambíguo deve parar para reconciliação, sem uma segunda compra.
Boas práticas de integração
1. Nunca hardcode o token
# Ruim — nunca faça isso
token = "a1b2c3d4..."
# Bom — sempre de variável de ambiente
import os
token = os.environ["SMSCODE_TOKEN"]
2. Backoff exponencial no polling
def poll_with_backoff(order_id: int, max_attempts: int = 20) -> str:
delay = 3.0 # segundos iniciais
last_seen_revision = -1
for attempt in range(max_attempts):
result = client.get_order_status(order_id)
revision = result.get("sms_revision")
message = result.get("otp_message")
if (
type(revision) is int
and revision > last_seen_revision
and isinstance(message, str)
and message.strip()
):
last_seen_revision = revision
return message
lifecycle_status = result["status"]
if lifecycle_status in ["COMPLETED", "EXPIRED", "CANCELED"]:
raise ValueError(f"Status final: {lifecycle_status}")
# ACTIVE e OTP_RECEIVED continuam em polling sem nova mensagem válida.
# Backoff exponencial com jitter para evitar thundering herd
import random
jitter = random.uniform(0, 1)
delay = min(delay * 1.5 + jitter, 30) # máximo 30s
time.sleep(delay)
raise TimeoutError("SMS não chegou")
3. Sempre cancele pedidos não utilizados
try:
result = client.get_sms_code(88)
# Usar o resultado...
except Exception as e:
# O método relê can_cancel antes de enviar o cancelamento
client.cancel_order(order_id)
raise
4. Verifique disponibilidade antes de operações em lote
Para criar muitos pedidos simultaneamente, consulte o catálogo primeiro:
def batch_verify(tasks: list[dict]) -> list[dict]:
"""Verifica disponibilidade antes de criar pedidos em lote."""
results = []
for task in tasks:
catalog_product_id = task["catalog_product_id"]
# Verificar disponibilidade
catalog = client.check_availability(task["country_id"], task["platform_id"])
if not catalog["available"]:
results.append({"outcome": "unavailable", **task})
continue
# Criar pedido apenas se disponível
try:
result = client.get_sms_code(catalog_product_id)
outcome = (
"pending_assignment"
if result.get("kind") == "pending_assignment"
else "success"
)
results.append({"outcome": outcome, "result": result, **task})
except Exception as e:
results.append({"outcome": "error", "error": str(e), **task})
return results
5. Logging estruturado para debugging
import logging
import json
logger = logging.getLogger("smscode")
def log_request(method: str, path: str, response: dict):
logger.info(json.dumps({
"method": method,
"path": path,
"success": response.get("success"),
"order_id": response.get("data", {}).get("id"),
"status": response.get("data", {}).get("status"),
}))
6. Gerenciamento de múltiplos pedidos simultâneos
Para operações paralelas, use threading ou asyncio:
import asyncio
import aiohttp
import uuid
async def get_sms_code_async(
session: aiohttp.ClientSession,
catalog_product_id: int
) -> dict:
# Criar pedido
body = {"catalog_product_id": catalog_product_id, "quantity": 1}
idempotency_key = str(uuid.uuid4())
async with session.post(
f"{BASE_URL}/orders/create",
json=body,
headers={"Idempotency-Key": idempotency_key}
) as response:
order = (await response.json())["data"]["orders"][0]
# `phone_number` é opcional/anulável até a atribuição: uma única leitura
# limitada com o mesmo id, nunca outro create pago.
phone = order.get("phone_number")
if not (isinstance(phone, str) and phone.strip()):
async with session.get(f"{BASE_URL}/orders/{order['id']}") as atual_response:
atual = (await atual_response.json()).get("data", {})
phone = atual.get("phone_number")
if not (isinstance(phone, str) and phone.strip()):
# pending_assignment: pedido resolvido e cobrado; não use nem recrie.
return {"kind": "pending_assignment", "order_id": order["id"]}
# Polling assíncrono
last_seen_revision = -1
for _ in range(60):
await asyncio.sleep(5)
async with session.get(
f"{BASE_URL}/orders/{order['id']}"
) as status_response:
status = (await status_response.json())["data"]
revision = status.get("sms_revision")
message = status.get("otp_message")
if (
type(revision) is int
and revision > last_seen_revision
and isinstance(message, str)
and message.strip()
):
last_seen_revision = revision
return {
"phone": phone,
"code": status.get("otp_code"),
"message": message
}
lifecycle_status = status["status"]
if lifecycle_status in ("COMPLETED", "CANCELED", "EXPIRED"):
raise RuntimeError(f"Pedido finalizado: {lifecycle_status}")
# ACTIVE e OTP_RECEIVED continuam em polling sem nova mensagem válida.
raise TimeoutError("SMS não chegou")
async def main():
token = os.environ["SMSCODE_TOKEN"]
headers = {"Authorization": f"Bearer {token}"}
timeout = aiohttp.ClientTimeout(total=30, connect=5)
async with aiohttp.ClientSession(headers=headers, timeout=timeout) as session:
# Executa 3 verificações em paralelo
tasks = [
get_sms_code_async(session, 88),
get_sms_code_async(session, 89),
get_sms_code_async(session, 90),
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
print(f"Erro: {result}")
elif result.get("kind") == "pending_assignment":
# Pedido resolvido e cobrado; não use o número nem crie outro.
print(f"pending_assignment: pedido {result['order_id']} ainda sem número")
else:
print(f"Código: {result['code']}")
asyncio.run(main())
Casos de uso avançados
Integração em pipeline de CI/CD
Para testes E2E automatizados que precisam de verificação de conta real:
# GitHub Actions — exemplo de step de verificação
- name: Verificar conta de teste
env:
SMSCODE_TOKEN: ${{ secrets.SMSCODE_TOKEN }}
run: |
python3 scripts/verify_test_account.py \
--catalog-product-id 88 \
--output-file /tmp/test-credentials.json
Monitoramento de saldo em produção
import requests
def check_balance_alert(min_balance: int = 100000) -> None:
"""Dispara alerta se saldo abaixo do mínimo."""
response = requests.get(
f"{BASE_URL}/balance",
headers=HEADERS
)
balance = response.json()["data"]["balance"]
if balance < min_balance:
print(
f"Saldo SMSCode crítico: Rp {balance:,} "
f"(mínimo configurado: {min_balance})"
)
FAQ
A API do SMSCode tem SDK oficial?
Os exemplos deste guia usam diretamente a API REST com clientes HTTP padrão. Consulte a documentação oficial atual antes de escolher um wrapper ou SDK; não dependa de uma promessa de roadmap.
Qual o limite de requisições (rate limit) da API?
Não fixe uma quota numérica no cliente. Se receber 429 Too Many Requests, aguarde o valor positivo indicado em Retry-After; se o header estiver ausente ou malformado, use um atraso padrão limitado.
A API suporta webhooks para receber SMS sem polling?
Sim. Valide X-Webhook-Signature no formato sha256={hex} sobre os bytes brutos do body, com comparação em tempo constante, antes de fazer parse do JSON. Processe e persista ou enfileire o evento de forma durável antes de responder 2xx. Polling continua útil como mecanismo de reconciliação.
Como testar a integração sem gastar muito crédito?
Não existe sandbox separado: paid creates usam a API e o saldo reais. Escolha no catálogo um produto ativo de baixo custo, limite a quantidade e reconcilie qualquer resposta ambígua com a mesma chave e o mesmo body antes de criar outro pedido.
A API funciona com qualquer linguagem de programação?
Sim — qualquer linguagem que consiga fazer requisições HTTP e parsear JSON funciona com a API do SMSCode. Além dos exemplos neste guia, você pode integrar com Ruby, Java, Go, Rust, Swift, Kotlin, ou qualquer outra. O padrão REST com JSON é universal.