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):
'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:
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:
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.
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.
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:
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).