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.
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.
| Caso | Por que SMS ajuda | O que revisar |
|---|---|---|
| Pedido e envio | Cliente pode perder email, mas precisa do status | URL errada, duplicados, opt-out |
| Lembrete de consulta | Reduz faltas e confusão de última hora | Fuso horário, horário sensível, consentimento |
| Alerta de incidente | Alcança plantão fora de Slack ou email | Tempestade de alertas, limites, escalonamento |
| Login e 2FA | Protege contas | Preferir Twilio Verify a OTP próprio |
| Confirmação de suporte | Mostra que a solicitação foi recebida | Evitar 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.
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.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.