Verificação por SMS OTP com Node.js e a APIBrasil: código de confirmação passo a passo

Verificação por SMS OTP com Node.js e a APIBrasil: código de confirmação passo a passo

Confirmar que o telefone informado realmente pertence ao usuário é uma das formas mais eficientes de reduzir cadastros falsos e proteger logins. O jeito mais popular de fazer isso é o SMS OTP: você envia um código de uso único por SMS e o usuário digita esse código no seu site ou app.

Neste tutorial você vai implementar uma verificação por SMS OTP com Node.js usando a API SMS OTP da APIBrasil, do envio do código à validação, incluindo o webhook que avisa o que aconteceu com cada mensagem.

Como funciona o fluxo de SMS OTP

  1. O usuário informa o celular.
  2. O seu back-end gera um código aleatório de 6 dígitos e guarda um hash dele com prazo de validade.
  3. O back-end chama a APIBrasil, que envia o SMS.
  4. A APIBrasil avisa o seu webhook sobre cada etapa do envio.
  5. O usuário digita o código, e o back-end compara com o hash guardado.

A API SMS OTP fica na categoria Comunicação do catálogo em doc.apibrasil.io, com preço a partir de R$ 0,10 por mensagem. Ela é a opção indicada para mensagens transacionais de alta prioridade, como códigos de verificação.

Pré-requisitos

  • Node.js 18 ou mais novo e o pacote express (npm install express).
  • Bearer Token da APIBrasil, em Credenciais no painel. Veja os primeiros passos.
  • Uma URL pública com HTTPS para o webhook. Em desenvolvimento, serviços de túnel como ngrok ou Cloudflare Tunnel resolvem.

Passo 1: a chamada de envio

POST https://gateway.apibrasil.io/api/v2/sms/send/credits
Authorization: Bearer SEU_TOKEN
Content-Type: application/json

{
  "tipo": "sms-otp",
  "number": "5531999999999",
  "message": "Seu código de verificação é 482913",
  "operator": "claro",
  "user_reply": true,
  "webhook_url": "https://seusite.com.br/webhook/sms"
}
CampoPara que serve
numberCelular com código do país e DDD, só números
messageTexto do SMS
operatorOperadora, como no exemplo do catálogo
user_replyPermite que o destinatário responda
webhook_urlURL que vai receber os avisos de status desta mensagem

A resposta traz o resultado do envio dentro de response, e não em data como na maioria das APIs:

Resposta JSON da API SMS OTP da APIBrasil com status processed e id da mensagem
O envio devolve um <code>id</code> e o status <code>processed</code>. Guarde o id para cruzar com o webhook.

Passo 2: gere e guarde o código com segurança (otp.js)

Três regras de segurança para OTP. Salve o código abaixo como otp.js:

  • Gere o código com crypto.randomInt, nunca com Math.random.
  • Guarde só o hash do código, com prazo de validade curto (5 minutos é comum).
  • Limite as tentativas de validação e os reenvios por número.
import crypto from "node:crypto";

const codigos = new Map(); // em produção, use Redis ou o seu banco

export function gerarOtp(numero) {
  const codigo = String(crypto.randomInt(0, 1_000_000)).padStart(6, "0");
  const hash = crypto.createHash("sha256").update(numero + codigo).digest("hex");
  codigos.set(numero, { hash, expira: Date.now() + 5 * 60_000, tentativas: 0 });
  return codigo;
}

export function validarOtp(numero, codigo) {
  const reg = codigos.get(numero);
  if (!reg || Date.now() > reg.expira) return false;
  if (++reg.tentativas > 5) {
    codigos.delete(numero);
    return false;
  }
  const hash = crypto.createHash("sha256").update(numero + codigo).digest("hex");
  const ok = crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(reg.hash));
  if (ok) codigos.delete(numero);
  return ok;
}

Passo 3: o servidor com envio, validação e webhook

Crie o arquivo server.js:

import express from "express";
import { gerarOtp, validarOtp } from "./otp.js";

const app = express();
app.use(express.json());

async function enviarSms(numero, texto) {
  const resp = await fetch("https://gateway.apibrasil.io/api/v2/sms/send/credits", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.APIBRASIL_TOKEN}`,
    },
    body: JSON.stringify({
      tipo: "sms-otp",
      number: numero,
      message: texto,
      operator: "claro",
      user_reply: false,
      webhook_url: process.env.WEBHOOK_URL,
    }),
    signal: AbortSignal.timeout(60_000),
  });
  if (resp.status === 402) throw new Error("Saldo insuficiente na APIBrasil");
  const json = await resp.json();
  if (!resp.ok || json.error) throw new Error(json.message || `HTTP ${resp.status}`);
  return json.response?.data; // { id, status, created_at }
}

app.post("/otp/enviar", async (req, res) => {
  const numero = String(req.body.numero || "").replace(/\D/g, "");
  if (!/^55\d{10,11}$/.test(numero)) return res.status(400).json({ erro: "Número inválido" });

  const codigo = gerarOtp(numero);
  try {
    const envio = await enviarSms(numero, `Seu código de verificação é ${codigo}. Ele expira em 5 minutos.`);
    console.log("POST /otp/enviar", numero, "→ id", envio?.id, `(${envio?.status})`);
    res.json({ enviado: true });
  } catch (e) {
    res.status(502).json({ erro: "Não foi possível enviar o SMS" });
  }
});

app.post("/otp/validar", (req, res) => {
  const numero = String(req.body.numero || "").replace(/\D/g, "");
  const ok = validarOtp(numero, String(req.body.codigo || ""));
  res.status(ok ? 200 : 401).json({ verificado: ok });
});

app.post("/webhook/sms", (req, res) => {
  const [aviso] = Array.isArray(req.body) ? req.body : [];
  res.sendStatus(200); // responda antes de processar
  if (!aviso) return;
  console.log("POST /webhook/sms", aviso.id, aviso.status);
});

app.listen(3000, () => console.log("Servidor OTP ouvindo na porta 3000"));

Passo 4: entenda os avisos do webhook

A documentação de webhooks da APIBrasil descreve quatro eventos para SMS, na ordem em que acontecem:

StatusSignificado
inserted_for_processingA mensagem entrou na fila, ainda não foi validada nem enviada
validO número passou na validação de formato e de bloqueio
sent_to_carrierA operadora recebeu a mensagem (não é a confirmação no aparelho)
replyO destinatário respondeu, e o texto vem junto

Três detalhes que a documentação faz questão de destacar:

  • O corpo do webhook é um array, com um aviso dentro. Por isso o const [aviso] = req.body.
  • Responda 200 antes de processar. Gravar no banco antes de responder pode transformar lentidão em timeout de webhook.
  • A chave de idempotência é id + status. O mesmo id chega uma vez por status. Deduplicar só pelo id descartaria avisos legítimos.
Log do servidor Node.js mostrando envio do código OTP por SMS, webhook de status e validação
O ciclo completo: pedido do código, avisos do webhook e validação.

Boas práticas de SMS OTP

  • Texto curto e claro: diga o que é o código, de quem é e quando expira. Nunca peça para o usuário compartilhar o código.
  • Limite de reenvio: um reenvio a cada 60 segundos e poucos por hora por número evitam abuso e custo.
  • Não revele se o número existe na sua base na tela de login.
  • Use HTTPS no webhook: o aviso carrega o número do destinatário e, no caso de resposta, o texto.

Perguntas frequentes

Qual a diferença entre SMS OTP, SMS Avulso e SMS Marketing?

São APIs diferentes no catálogo, pensadas para usos diferentes. A SMS OTP é voltada a mensagens transacionais de alta prioridade, como códigos. Para campanhas, use a SMS Marketing.

Dá para testar sem gastar?

Consulte a página da API SMS OTP em doc.apibrasil.io para ver o suporte a homologação desta API. O corpo de exemplo do catálogo não inclui o campo homolog, então confirme antes de rodar testes automatizados.

Próximos passos

Use a verificação por SMS junto com a consulta de CPF na Receita Federal para um cadastro mais seguro, ou veja como enviar SMS com PHP usando a SDK da APIBrasil.

Loading

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *