Como Usar a API do SMSCode (2026)

Como Usar a API do SMSCode (2026)

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:

  1. Conta ativa no SMSCodeCrie em smscode.gg com apenas um e-mail, sem documentos
  2. Saldo de crédito — Adicione por um método exibido na página Deposit da sua conta; opções e condições podem mudar
  3. Token de API — Disponível em Configurações > API do painel (64 caracteres hexadecimais)
  4. Ferramenta para chamadas HTTP — cURL para testes, ou cliente HTTP na sua linguagem preferida

Como encontrar o token de API

  1. Faça login em smscode.gg
  2. Clique no seu avatar ou e-mail no canto superior direito
  3. Vá em “Settings” > seção “API”
  4. 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 um Retry-After positivo 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.

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 IDR
  • available: quantidade aproximada disponível
  • active: indica se o produto está ativo
  • catalog_product_id: identificador usado na criação do pedido
  • country_id e platform_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
WhatsApp wa
Telegram tg
Instagram ig
Google/Gmail go
Facebook 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 subsequentes
  • phone_number: número virtual completo com DDI quando já atribuído; é opcional/anulável até a atribuição terminar
  • status: começa como ACTIVE
  • expires_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 SMS
  • OTP_RECEIVED: mensagem recebida; otp_code pode ser null
  • COMPLETED: pedido concluído após a entrega
  • EXPIRED: janela do pedido encerrada
  • CANCELED: 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.

Pronto para experimentar o SMSCode?

Crie uma conta e obtenha seu primeiro número virtual em menos de dois minutos.

Começar →