Webhook de SMS na prática: status de entrega em tempo real

本文目前仅提供葡萄牙语版本。

Um webhook de SMS é um endpoint HTTP da sua aplicação que o provedor chama automaticamente a cada evento relevante da mensagem: em vez de você consultar o status em loop (polling), a plataforma envia um POST com o relatório de entrega (DLR) ou com a resposta do destinatário assim que o evento acontece. Na SMSGo, você cadastra a URL uma única vez via PUT /v1/account/webhook e passa a receber dois eventos — sms.status e sms.reply — assinados com HMAC-SHA256 no header X-SMSGo-Signature.

Por que polling não escala

Depois de enviar um SMS via API, a resposta imediata diz apenas que a mensagem entrou na fila. O que importa — entregue, falhou, respondida — chega depois, no ritmo da operadora. Consultar o endpoint de detalhe a cada poucos segundos multiplica requisições inúteis, atrasa a reação do seu sistema e ainda esbarra em rate limits.

O volume só agrava o problema: segundo a Juniper Research, o tráfego global de mensagens de negócios (A2P) deve crescer de 2 trilhões de mensagens em 2025 para quase 3 trilhões até 2030. Com webhook, a relação se inverte: um evento, um POST, zero desperdício.

Os dois eventos: sms.status e sms.reply

A SMSGo envia um POST em JSON para a sua URL em duas situações:

  • sms.status — relatório de entrega (DLR). O campo data traz sendId, phone e status, que pode ser delivered, failed ou in_progress.
  • sms.reply — resposta do destinatário (mensagem MO). O data traz fromPhone, message e receivedAt.

O corpo completo tem sempre o mesmo envelope:

{
  "id": "3f1c0a2e-uuid-do-evento",
  "event": "sms.status",
  "data": { "sendId": "uuid-do-envio", "phone": "5511999990000", "status": "delivered" },
  "sentAt": "2026-07-02T12:00:00.000Z"
}

O id identifica o evento (ele também vai no header X-SMSGo-Event-Id) e o sentAt carimba o momento do disparo em ISO-8601 UTC. O sms.reply merece atenção especial: segundo a Gartner, o SMS alcança taxas de resposta de até 45%, então tratar as respostas — inclusive pedidos de opt-out, uma exigência prática da LGPD — não é opcional.

Configurando o webhook na SMSGo

Cadastre a URL no painel (Minha conta, seção API) ou direto pela API:

curl -X PUT https://api.smsgo.com.br/v1/account/webhook \
  -H "Authorization: Bearer SEU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://app.suaempresa.com.br/webhooks/smsgo" }'

Três regras valem em produção: a URL precisa ser HTTPS, apontar para um host público (endereços internos são recusados) e responder sem redirecionamentos — redirects não são seguidos. Ao configurar, você recebe um secret: guarde-o em variável de ambiente, pois é com ele que a assinatura de cada entrega é gerada.

Verificando a assinatura HMAC-SHA256 (Node.js/Express)

Uma URL pública aceita POST de qualquer um — sem verificação, um atacante pode forjar um evento de entrega ou uma resposta falsa. Por isso toda requisição da SMSGo traz o header X-SMSGo-Signature: sha256=..., o HMAC-SHA256 do corpo bruto calculado com o seu secret. O receptor DEVE validar antes de confiar:

const express = require('express')
const crypto = require('crypto')

const app = express()

app.post(
  '/webhooks/smsgo',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-SMSGo-Signature') || ''
    const expected = 'sha256=' + crypto
      .createHmac('sha256', process.env.SMSGO_WEBHOOK_SECRET)
      .update(req.body) // corpo BRUTO, antes de qualquer parse
      .digest('hex')

    const a = Buffer.from(signature)
    const b = Buffer.from(expected)
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send('assinatura invalida')
    }

    const event = JSON.parse(req.body.toString('utf8'))

    res.sendStatus(200) // confirme rapido; processe depois

    if (event.event === 'sms.status') {
      // event.data: { sendId, phone, status }
    }
    if (event.event === 'sms.reply') {
      // event.data: { fromPhone, message, receivedAt }
    }
  }
)

app.listen(3000)

Dois detalhes fazem a diferença. Primeiro, o HMAC é calculado sobre o corpo bruto: se um middleware de JSON reserializar o payload antes da verificação, a assinatura nunca vai bater — daí o express.raw na rota. Segundo, a comparação usa timingSafeEqual, imune a ataques de temporização, em vez de um simples comparador de strings.

Idempotência e proteção contra replay

A assinatura prova autenticidade, não frescor: uma entrega legítima capturada poderia ser reenviada depois. Como id e sentAt estão dentro do corpo assinado, feche as duas brechas após validar o HMAC:

  1. Rejeite eventos antigos: descarte se sentAt estiver fora de uma janela de tolerância (5 minutos é um bom padrão) em relação ao seu relógio.
  2. Deduplique por id: a SMSGo reenvia o evento com backoff quando seu endpoint não responde 2xx, então o mesmo evento pode chegar mais de uma vez. Guarde os id processados e ignore repetições; para sms.status, o par sendId + status também funciona como chave natural.
  3. Responda 2xx imediatamente e jogue o processamento pesado para uma fila. Endpoint lento vira timeout, e timeout vira retry — ou seja, mais duplicatas.

O que fazer com cada status

  • delivered — confirme o fluxo de negócio: marque a notificação como entregue, alimente métricas de campanha, encerre o ciclo do OTP.
  • failed — acione o plano B: tentar novamente, cair para outro canal (e-mail, push) ou marcar o número como inválido para higienizar sua base.
  • in_progress — estado intermediário; útil para exibir "enviando" na sua interface, sem ação de negócio.
  • sms.reply — roteie a resposta: palavras como SAIR devem alimentar sua lista de opt-out na hora; o resto pode abrir um atendimento ou seguir um fluxo de duas vias.

Erros comuns ao receber webhooks

  • Verificar a assinatura sobre o JSON já parseado em vez do corpo bruto — a causa mais comum de "assinatura não bate".
  • Comparar o HMAC com == em vez de comparação de tempo constante.
  • Processar de forma síncrona dentro do handler e estourar o timeout do provedor.
  • Ignorar duplicatas e disparar duas vezes o mesmo efeito colateral (dois e-mails, dois estornos).
  • Testar com URL http ou host interno — em produção a SMSGo exige HTTPS em host público.

Próximos passos

Se você ainda não fez o primeiro disparo, comece pelo guia de como enviar SMS via API — em seguida, o webhook fecha o ciclo com o status de cada mensagem. Para fluxos de verificação, veja como enviar OTP por SMS. Crie sua conta na SMSGo: R$ 10 de créditos grátis no cadastro, sem cartão, e webhooks assinados desde a primeira mensagem.

Equipe Editorial SMSGo
Equipe Editorial SMSGoTime de Conteúdo

O time da SMSGo escreve sobre API de SMS, OTP, webhooks e boas práticas de mensageria para empresas brasileiras.