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.
Artigos relacionados
Claude Code e AWS API Gateway: HTTP API com SAM, CORS, logs, autenticação e limites
Implemente AWS API Gateway com Claude Code, SAM, CORS, logs, auth e throttling com exemplos práticos.
Claude Code × AWS Lambda Guia Completo | Da Geração de Funções à Automação do Deploy
Crie AWS Lambda com Claude Code: Node.js 24, SAM, IAM, API Gateway, S3, logs e falhas comuns.
Design prático de API com Claude Code: OpenAPI, testes e breaking changes
Projete REST APIs confiáveis com Claude Code: OpenAPI, mock, testes, versionamento, segurança e armadilhas.
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.