Pular para o conteúdo
📖 COOKBOOK

WhatsApp OTP em 8 linhas.

Único do SuperDB: OTP entregue via WhatsApp em vez de SMS. Pra apps brasileiros, é gigantesco — usuário não fecha conta no WhatsApp. Mostra send, verify, rate limit e como cair para outro canal se o número não tem WhatsApp.

O que vamos fazer

  • Input de telefone com mask brasileira (+55 (DD) 9XXXX-XXXX)
  • Botão "Enviar código" que dispara OTP via WhatsApp em ~3 segundos
  • Input de 6 dígitos pro usuário digitar o código recebido
  • Verify → sessão criada e usuário logado
  • Como cair para outro canal no seu app quando o número não tem WhatsApp

Pré-requisitos

  • Plano Pro+ (o Free não inclui WhatsApp)
  • Um número de WhatsApp pareado em Dashboard > Auth > WhatsApp do projeto — ou nenhum, e o código sai pelo número da plataforma
  • Número de teste verificado com a Meta (ou número de produção aprovado)
  • Template de mensagem aprovado pela Meta (o SuperDB já fornece um padrão)
💡

Por que importa pra Brasil: 99% dos brasileiros adultos têm WhatsApp e checam em minutos. SMS no Brasil custa 4× mais que WhatsApp e tem taxa de entrega menor (operadoras filtram). Pra apps B2C, WhatsApp OTP reduz drop-off de signup em 20-30%.

Passo 1 — Frontend com input formatado

Use uma lib de mask ou regex inline. Importante: enviar pro backend já em formato E.164 (+5511999998888):

app/login/whatsapp-form.tsx
'use client'
import { useState } from 'react'
import { createBrowserClient } from '@superdb/client'

function formatBR(raw: string) {
  const digits = raw.replace(/\D/g, '').slice(0, 11)
  if (digits.length <= 2) return digits
  if (digits.length <= 7) return `(${digits.slice(0, 2)}) ${digits.slice(2)}`
  return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`
}

function toE164(formatted: string) {
  return '+55' + formatted.replace(/\D/g, '')
}

export function WhatsAppForm() {
  const [phone, setPhone] = useState('')
  const [enviado, setEnviado] = useState(false)
  const [code, setCode] = useState('')

  const db = createBrowserClient(
    process.env.NEXT_PUBLIC_SUPERDB_URL!,
    process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
  )

  async function enviar() {
    const { error } = await db.auth.signInWithOtp({
      phone: toE164(phone),
      options: { channel: 'whatsapp' },
    })
    if (!error) setEnviado(true)
  }

  async function verificar() {
    const { error } = await db.auth.verifyOtp({
      // O MESMO alvo do envio. O WhatsApp normaliza o número na entrega
      // (chega a tirar o nono dígito), mas o que casa o código é o que
      // você mandou — use toE164 nos dois lugares.
      phone: toE164(phone),
      code,
    })
    if (!error) location.href = '/app'
  }

  if (!enviado) return (
    <>
      <input value={phone} onChange={(e) => setPhone(formatBR(e.target.value))}
             placeholder="(11) 99999-8888" />
      <button onClick={enviar}>Enviar código via WhatsApp</button>
    </>
  )

  return (
    <>
      <p>Código enviado pro WhatsApp {phone}</p>
      <input value={code} onChange={(e) => setCode(e.target.value)}
             maxLength={6} placeholder="000000" />
      <button onClick={verificar}>Verificar</button>
    </>
  )
}

Passo 2 — Como o envio funciona

O SuperDB recebe a chamada, verifica rate limit, e envia via provider configurado. Template típico aprovado pela Meta:

A mensagem que o usuário recebe
Seu código de acesso ao Meu App é: 478291

Não compartilhe este código com ninguém.
Válido por 10 minutos.

O texto sai assinado com o nome do seu projeto quando você pareia um número próprio em Dashboard → Auth → WhatsApp do projeto. Sem número próprio, ele sai do WhatsApp do SuperDB, assinado "SuperDB".

Passo 3 — Verify e criação da sessão

Quando o user digita o código, o verifyOtp valida server-side e cria a sessão:

terminal — debug com cURL
curl -X POST "$SUPERDB_AUTH_URL/auth/v1/signin/otp/verify" \
  -H "X-SuperDB-Project: SEU_SLUG" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+5511999998888",
    "code": "478291"
  }'

# Retorna:
# { "access_token": "...", "refresh_token": "...", "user": {...} }

O verify aceita phone ou email — o mesmo alvo que você usou no envio. Não existe campo type: o canal só importa no envio.

Passo 4 — Quando o WhatsApp não dá conta

Nem todo número tem WhatsApp ativo, e o envio pode falhar. O fallback é do seu app — a API não troca de canal sozinha. Os códigos de erro que ela devolve são whatsapp_send_failed (o envio não saiu) e whatsapp_not_configured (o canal não está disponível na plataforma). Caia para email, que está sempre de pé; sms depende de a plataforma ter provedor configurado e devolve sms_not_configured quando não tem.

app/login/whatsapp-form.tsx
async function enviarComFallback() {
  const e164 = toE164(phone)

  // 1ª tentativa: WhatsApp
  const r1 = await db.auth.signInWithOtp({
    phone: e164,
    options: { channel: 'whatsapp' },
  })

  // Os dois códigos que a API realmente emite quando o WhatsApp não serve.
  const semWhats = ['whatsapp_send_failed', 'whatsapp_not_configured']
  if (r1.error && semWhats.includes(r1.error.code)) {
    // Cai para e-mail, que não depende de provedor externo.
    const r2 = await db.auth.signInWithOtp({ email: emailDoUsuario })
    if (r2.error) return alert('Não foi possível enviar o código')
    setCanal('email')  // mostra "Código enviado por e-mail"
  }

  setEnviado(true)
}

Resultado

App brasileiro com login de 30 segundos via WhatsApp. Em testes A/B em apps B2C brasileiros (delivery, fintech, marketplace), WhatsApp OTP supera SMS em conversão de signup.

WhatsApp · +55 11 4002-8922
v0.4.2
Seu código de acesso ao <strong>Meu App</strong> é:
478 291
Não compartilhe. Válido por 10 minutos.
14:32 ✓✓

Variações

Guardar dados do cadastro

O signInWithOtp aceita só email ou phone mais options.channel — não há campo para metadados. Quem entra pela primeira vez é criado com o telefone confirmado; grave o resto do perfil na sua tabela, depois da sessão existir:

signup-whatsapp.ts
await db.auth.verifyOtp({ phone: e164, code })   // sessão pronta

const { data: { user } } = await db.auth.getUser()
await db.from('perfis').upsert({
  user_id: user.id,
  nome: 'Fulano',
  origem: 'landing_b',
})

Rate limit

Fixo na plataforma, não configurável no dashboard: 3 códigos por destino a cada hora, 100 por projeto/hora, 10 pedidos por IP/hora e 10 tentativas de verificação a cada 5 min. Código válido por 10 minutos. Estourar devolve 429 rate_limited.

Erros comuns

Número sem código do país

O parâmetro phone precisa estar em E.164: +5511999998888. Sem o +, sem o 55, ou com hífens/parênteses → 400 invalid_request. Sempre normalize antes de enviar (use a função toE164 mostrada no Passo 1).

O envio falhou

Erro 503 whatsapp_send_failed: o código não saiu. A causa mais comum é o número de destino não ter WhatsApp; a outra é o número de origem ter sido desconectado. Trate caindo para email, como no Passo 4.

Se o seu projeto usa número próprio e ele caiu, o envio volta ao número da plataforma sozinho — o login não para — e o painel mostra Desconectado em Auth → WhatsApp do projeto.

⚠️

Em produção: o rate limit da plataforma (3 por número/hora) segura o abuso mais grosseiro, mas ele é por destino. Se o seu formulário de login é aberto, coloque um captcha antes do botão de enviar — sem isso um script varre números às suas custas de reputação do remetente.

Usuário bloqueou seu número

Se o user já bloqueou o número Business da sua empresa, o WhatsApp aceita o envio mas a mensagem nunca chega. Você não consegue detectar isso. Mitigação: depois de 2min sem confirm, ofereça botão "não recebi, enviar por SMS" — não fique repetindo o envio via WhatsApp.

ℹ️

Sobre conformidade LGPD: o número de telefone é dado pessoal. Mostre o aviso de privacidade ANTES do botão de envio, não depois. O SuperDB já trata o número como PII e cifra em repouso (entregue).

Essa página ajudou?