Use Cases (Atualizado: 02/06/2026)

Twilio SMS com Claude Code: notificações, Verify e Webhooks em produção

Implemente Twilio SMS com Claude Code: E.164, consentimento, idempotência, retries, Verify e callbacks.

Twilio SMS com Claude Code: notificações, Verify e Webhooks em produção

SMS ainda é um canal útil quando o usuário não está com o app aberto. Ele funciona bem para envio de pedidos, lembretes de consulta, alertas de incidente, verificação de login e respostas urgentes de suporte.

Uma integração com Twilio SMS parece pequena, mas produção não é apenas client.messages.create. Você precisa validar números, registrar consentimento, evitar envios duplicados, fazer retry com critério, receber status callbacks, validar assinatura de Webhook e manter logs sem expor dados pessoais.

Este guia mostra como pedir ao Claude Code uma integração prática em Express + TypeScript. O exemplo cobre SMS de saída, Twilio Verify, callbacks de status, idempotência, retries, logs, segurança e armadilhas comuns. Para o restante do sistema, veja também implementação de autenticação, implementação de Webhooks e gestão de segredos.

Twilio SMS em termos simples

Twilio oferece comunicação como API. Seu backend pede à Twilio para enviar este texto, deste remetente, para este número. A Twilio entrega a mensagem à rede das operadoras e retorna um Message SID, o identificador que você usa para suporte e rastreamento.

Os números devem estar no formato E.164: sinal de mais, código do país e número, como +15558675310 ou +819012345678. Esse é o formato seguro para API, não necessariamente a forma local exibida ao usuário. Use a orientação oficial da Twilio sobre formatação internacional de números.

A primeira resposta da API não significa que o SMS chegou. O status pode passar por queued, sent, delivered, undelivered ou failed. A Twilio pode chamar seu endpoint de status callback quando isso muda. Consulte Programmable Messaging, o tutorial de SMS com Node.js, Messaging Webhooks e o guia de outbound status callbacks.

Casos de uso reais

Não comece com um helper genérico de SMS. Comece pelo evento de negócio e pelas regras de falha.

CasoPor que SMS ajudaO que revisar
Pedido e envioCliente pode perder email, mas precisa do statusURL errada, duplicados, opt-out
Lembrete de consultaReduz faltas e confusão de última horaFuso horário, horário sensível, consentimento
Alerta de incidenteAlcança plantão fora de Slack ou emailTempestade de alertas, limites, escalonamento
Login e 2FAProtege contasPreferir Twilio Verify a OTP próprio
Confirmação de suporteMostra que a solicitação foi recebidaEvitar dados sensíveis no corpo

Preços, países suportados, registro de remetente, regras tipo A2P e exigências regulatórias mudam. Este artigo não fixa esses dados. Antes de lançar, confira a Twilio Console, a documentação oficial atual e a revisão jurídica ou de compliance.

Prompt para Claude Code

Peça comportamento operacional, não só uma chamada de API.

Implement Twilio SMS notifications in Express + TypeScript.

Requirements:
- Read Twilio credentials, sender number, and Verify Service SID from env vars
- Validate phone numbers in E.164 format with Zod
- Add POST /api/order-shipped-sms for order shipment SMS
- Use eventId as the idempotency key so duplicate events do not send twice
- Retry only 429 and 5xx-style transient failures
- Never log full phone numbers, full message bodies, Auth Tokens, or OTP codes
- Receive status callbacks at POST /twilio/status-callback
- Require Twilio signature validation in production
- Add Twilio Verify start/check endpoints
- Include .env.example, package.json, run commands, and curl examples

Idempotência significa que o mesmo evento pode ser processado novamente sem gerar outro efeito externo. Em SMS isso é essencial: filas, Webhooks reenviados, batches e ações manuais de suporte podem repetir o mesmo evento.

flowchart LR
  A["Atualização de pedido"] --> B["Checagem de idempotência"]
  B --> C["Twilio Messaging API"]
  C --> D["Entrega SMS"]
  C --> E["Salvar Message SID"]
  D --> F["Status Callback"]
  F --> G["Validar assinatura"]
  G --> H["Atualizar log de entrega"]
  I["Verificação de login"] --> J["Twilio Verify"]

Criar o projeto mínimo

O projeto abaixo pode ser copiado e executado. Sem credenciais reais da Twilio, o envio não terá sucesso, mas você consegue validar ambiente, entrada, duplicidade e parsing do callback local.

mkdir twilio-sms-demo
cd twilio-sms-demo
npm init -y
npm install express twilio dotenv zod
npm install -D typescript tsx @types/express
{
  "type": "module",
  "scripts": {
    "dev": "tsx src/app.ts"
  },
  "dependencies": {
    "dotenv": "latest",
    "express": "latest",
    "twilio": "latest",
    "zod": "latest"
  },
  "devDependencies": {
    "@types/express": "latest",
    "tsx": "latest",
    "typescript": "latest"
  }
}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
# .env.example
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=replace-with-your-auth-token
TWILIO_FROM_NUMBER=+15551234567
TWILIO_VERIFY_SERVICE_SID=VAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PUBLIC_BASE_URL=https://example.ngrok-free.app
REQUIRE_TWILIO_SIGNATURE=true
PORT=3000

PUBLIC_BASE_URL deve ser uma URL HTTPS acessível pela Twilio. Para desenvolvimento local, use ngrok ou Cloudflare Tunnel. A validação de assinatura depende da URL exata, então confira protocolo, proxy, query string e barra final.

Implementar SMS, idempotência e callbacks

Crie src/app.ts e cole este código. O demo usa Map em memória; em produção use PostgreSQL, Redis, DynamoDB ou outro armazenamento durável com restrição única na chave de idempotência.

import "dotenv/config";
import express from "express";
import twilio from "twilio";
import { z } from "zod";

const e164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, {
  message: "Use E.164 format, for example +819012345678.",
});

const envSchema = z.object({
  TWILIO_ACCOUNT_SID: z.string().regex(/^AC[a-fA-F0-9]{32}$/),
  TWILIO_AUTH_TOKEN: z.string().min(20),
  TWILIO_FROM_NUMBER: e164Schema,
  TWILIO_VERIFY_SERVICE_SID: z.string().regex(/^VA[a-fA-F0-9]{32}$/).optional(),
  PUBLIC_BASE_URL: z.string().url(),
  REQUIRE_TWILIO_SIGNATURE: z.enum(["true", "false"]).default("true"),
  PORT: z.coerce.number().int().positive().default(3000),
});

const env = envSchema.parse(process.env);
const client = twilio(env.TWILIO_ACCOUNT_SID, env.TWILIO_AUTH_TOKEN);
const app = express();

type Delivery = {
  status: "pending" | "sent" | "failed";
  attempts: number;
  updatedAt: string;
  sid?: string;
  error?: string;
};

const deliveries = new Map<string, Delivery>();

const orderSmsSchema = z.object({
  eventId: z.string().min(6).max(120),
  phone: e164Schema,
  orderId: z.string().min(1).max(80),
  trackingUrl: z.string().url().optional(),
  consentAt: z.string().datetime(),
});

const statusCallbackSchema = z.object({
  MessageSid: z.string().min(2),
  MessageStatus: z.string().min(2),
  To: z.string().optional(),
  ErrorCode: z.string().optional(),
}).passthrough();

function maskPhone(phone: string) {
  return phone.replace(/\d(?=\d{4})/g, "*");
}

function delay(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function getErrorStatus(error: unknown) {
  if (typeof error === "object" && error && "status" in error) {
    return Number((error as { status?: number }).status ?? 0);
  }
  return 0;
}

function getErrorMessage(error: unknown) {
  return error instanceof Error ? error.message : String(error);
}

function shouldRetry(error: unknown) {
  const status = getErrorStatus(error);
  return status === 429 || status >= 500;
}

async function sendSmsWithRetry(params: {
  to: string;
  body: string;
  statusCallback: string;
  maxAttempts?: number;
}) {
  const maxAttempts = params.maxAttempts ?? 3;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      const message = await client.messages.create({
        body: params.body,
        from: env.TWILIO_FROM_NUMBER,
        statusCallback: params.statusCallback,
        to: params.to,
      });

      return { sid: message.sid, attempts: attempt };
    } catch (error) {
      if (attempt === maxAttempts || !shouldRetry(error)) {
        throw error;
      }
      await delay(500 * attempt);
    }
  }

  throw new Error("SMS retry loop ended unexpectedly.");
}

function verifyTwilioSignature(req: express.Request) {
  const signature = req.header("x-twilio-signature") ?? "";
  const callbackUrl = new URL(req.originalUrl, env.PUBLIC_BASE_URL).toString();
  return twilio.validateRequest(env.TWILIO_AUTH_TOKEN, signature, callbackUrl, req.body);
}

app.use(express.json());

app.post("/api/order-shipped-sms", async (req, res) => {
  const parsed = orderSmsSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({
      error: "invalid_request",
      details: parsed.error.flatten(),
    });
  }

  const input = parsed.data;
  const idempotencyKey = `order-shipped:${input.eventId}`;
  const existing = deliveries.get(idempotencyKey);

  if (existing?.status === "sent") {
    return res.status(200).json({
      duplicate: true,
      sid: existing.sid,
      status: existing.status,
    });
  }

  if (existing?.status === "pending") {
    return res.status(202).json({
      duplicate: true,
      status: existing.status,
    });
  }

  deliveries.set(idempotencyKey, {
    attempts: 0,
    status: "pending",
    updatedAt: new Date().toISOString(),
  });

  const trackingText = input.trackingUrl ? ` Tracking: ${input.trackingUrl}` : "";
  const body = `Your order ${input.orderId} has shipped.${trackingText}`;
  const statusCallback = new URL("/twilio/status-callback", env.PUBLIC_BASE_URL).toString();

  try {
    const result = await sendSmsWithRetry({
      body,
      statusCallback,
      to: input.phone,
    });

    deliveries.set(idempotencyKey, {
      attempts: result.attempts,
      sid: result.sid,
      status: "sent",
      updatedAt: new Date().toISOString(),
    });

    console.log("sms_sent", {
      idempotencyKey,
      sid: result.sid,
      to: maskPhone(input.phone),
    });

    return res.status(202).json({ accepted: true, sid: result.sid });
  } catch (error) {
    deliveries.set(idempotencyKey, {
      attempts: 3,
      error: getErrorMessage(error),
      status: "failed",
      updatedAt: new Date().toISOString(),
    });

    console.error("sms_failed", {
      idempotencyKey,
      message: getErrorMessage(error),
      status: getErrorStatus(error),
      to: maskPhone(input.phone),
    });

    return res.status(502).json({ error: "sms_delivery_failed" });
  }
});

app.post("/twilio/status-callback", express.urlencoded({ extended: false }), (req, res) => {
  if (env.REQUIRE_TWILIO_SIGNATURE === "true" && !verifyTwilioSignature(req)) {
    return res.status(403).send("invalid signature");
  }

  const parsed = statusCallbackSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).send("invalid callback");
  }

  console.log("twilio_status", {
    errorCode: parsed.data.ErrorCode,
    sid: parsed.data.MessageSid,
    status: parsed.data.MessageStatus,
    to: parsed.data.To ? maskPhone(parsed.data.To) : undefined,
  });

  return res.status(204).send();
});

app.listen(env.PORT, () => {
  console.log(`Twilio SMS demo listening on http://localhost:${env.PORT}`);
});

Inicie o servidor e envie uma requisição. Entrega real exige credenciais válidas da Twilio, remetente, callback público e destino permitido pela conta.

npm run dev
curl -X POST http://localhost:3000/api/order-shipped-sms \
  -H "Content-Type: application/json" \
  -d '{
    "eventId": "order_1001_shipped_v1",
    "phone": "+15558675310",
    "orderId": "1001",
    "trackingUrl": "https://example.com/track/1001",
    "consentAt": "2026-06-02T09:00:00.000Z"
  }'

Se repetir o mesmo eventId, a API retorna o estado existente em vez de enviar outro SMS. Em produção, grave esse estado em banco durável.

Para testar apenas o formato do callback local, use temporariamente REQUIRE_TWILIO_SIGNATURE=false. Em produção, mantenha true.

curl -X POST http://localhost:3000/twilio/status-callback \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "MessageSid=SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  --data-urlencode "MessageStatus=delivered" \
  --data-urlencode "To=+15558675310"

Usar Twilio Verify para OTP

Para login e 2FA, não comece gerando seis dígitos em casa. OTP envolve expiração, limite de reenvio, proteção contra força bruta, canais e auditoria. Twilio Verify e a Verification API foram feitos para isso.

Adicione este código antes de app.listen em src/app.ts.

const verifyStartSchema = z.object({
  phone: e164Schema,
});

const verifyCheckSchema = z.object({
  code: z.string().min(4).max(10),
  phone: e164Schema,
});

function requireVerifyServiceSid() {
  if (!env.TWILIO_VERIFY_SERVICE_SID) {
    throw new Error("TWILIO_VERIFY_SERVICE_SID is required for Verify.");
  }
  return env.TWILIO_VERIFY_SERVICE_SID;
}

app.post("/api/verify/start", async (req, res) => {
  const parsed = verifyStartSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({ error: "invalid_request" });
  }

  const verification = await client.verify.v2
    .services(requireVerifyServiceSid())
    .verifications.create({
      channel: "sms",
      to: parsed.data.phone,
    });

  return res.status(202).json({ sid: verification.sid, status: verification.status });
});

app.post("/api/verify/check", async (req, res) => {
  const parsed = verifyCheckSchema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({ error: "invalid_request" });
  }

  const check = await client.verify.v2
    .services(requireVerifyServiceSid())
    .verificationChecks.create({
      code: parsed.data.code,
      to: parsed.data.phone,
    });

  return res.json({ approved: check.status === "approved", status: check.status });
});

Após aprovação do Verify, atualize sua própria tabela de usuários, por exemplo phoneVerifiedAt ou mfaEnabledAt. Para o limite completo de autenticação, combine com o guia de autenticação e o guia de Zod.

Consentimento, compliance e segurança

SMS chega diretamente a um telefone pessoal. Registre o que o usuário aceitou receber, onde aceitou e como opt-out ou supressão são tratados. As exigências variam por país, remetente e conteúdo, então use as docs atuais da Twilio e revisão jurídica.

Nunca coloque Account SID, Auth Token, OTP, número completo ou corpo completo em código, prompts, capturas ou logs. .env fica fora do Git e produção injeta segredos via plataforma ou secret manager. Logs normalmente precisam de Message SID, event ID, tipo de mensagem, número mascarado, código de erro da Twilio, tentativas e timestamp.

Armadilhas comuns

Erros comuns: enviar número local sem E.164, deixar fila duplicar SMS, expor callback sem assinatura, implementar OTP próprio e registrar logs sem Message SID. Para assíncrono, leia sistemas de fila; para defesa, veja boas práticas de segurança.

Prompt de revisão

Review this Twilio SMS implementation before production.

Check:
- E.164 validation always runs before sending
- Consent timestamp and message purpose are tracked
- eventId idempotency holds under parallel requests
- Only 429 and 5xx transient failures are retried
- Twilio status callback signature validation is required in production
- Auth Tokens, OTP codes, full phone numbers, and full bodies never reach logs
- Pricing, countries, or regulatory rules are not hard-coded in comments
- Support can trace a failure by Message SID

Este tema também é bom para monetização porque o leitor costuma precisar de mais que uma função: autenticação, filas, Webhooks, logs, CLAUDE.md e revisão. ClaudeCodeLab pode transformar isso em fluxo de repositório real com treinamento e consultoria Claude Code.

Resumo

Twilio SMS começa com uma chamada curta, mas qualidade de produção depende de E.164, consentimento, idempotência, retries, assinatura de callback e logs privados. Dê esses requisitos ao Claude Code desde o primeiro prompt.

Na verificação prática deste artigo, o fluxo local validou E.164, tratamento de eventId duplicado, parsing de Status Callback e logs mascarados. A entrega real ainda depende de credenciais Twilio, remetente, país de destino e regras atuais da Twilio; antes de lançar, teste com um número controlado e siga o Message SID até o callback.

#Claude Code #Twilio #SMS #notificações #integração API
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.