O que vamos fazer
- Input de telefone com mask brasileira (
+55 (DD) 9XXXX-XXXX) - Botão "Enviar código" que manda o código de 6 dígitos pelo WhatsApp
- 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 ou Escala: o login por WhatsApp faz parte deles (veja Preços). Nos outros planos, o pedido responde
403 whatsapp_not_in_plane nenhum código é enviado. @superdb/client0.2.2 ou mais novo, com o cliente criado com oproject.- Opcional: o WhatsApp do seu negócio pareado em Autenticação → WhatsApp do projeto (também só nos planos que incluem o login por WhatsApp). Sem ele, o código sai pelo número do SuperDB, assinado "SuperDB" — e esse número, dividido entre todos os projetos, tem um teto de envios (veja Rate limit).
- Nada na Meta: o pareamento é o mesmo de conectar o WhatsApp Web (no celular, Configurações → Aparelhos conectados → Conectar aparelho, lendo o QR ou digitando o código de pareamento que o painel mostra), sem conta Business API nem template aprovado.
Por que WhatsApp: para o público brasileiro, é o aplicativo que já está aberto, e o código chega onde a pessoa olha. De brinde, quem digitou o código comprovou que tem aquele número: o telefone da conta nasce confirmado.
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 { db } from '@/lib/superdb' // createClient(url, anonKey, { project })
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('')
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 confere os limites, gera o código (válido por 10 minutos) e manda uma mensagem curta, de texto fixo:
Meu App: seu código de acesso é *478291*. Não compartilhe com ninguém. Vale por 10 minutos.
O *478291* aparece em negrito no WhatsApp. Com o seu WhatsApp pareado em
Autenticação → WhatsApp do projeto, a mensagem sai do seu número e leva o nome
do projeto. Sem número próprio, ela sai do WhatsApp do SuperDB, assinada SuperDB:,
de um número que o seu usuário não conhece: vale avisar na tela.
O texto não é personalizável, de propósito. O envio é pelo WhatsApp Web, não pela API oficial da Meta, e o que faz o WhatsApp banir um número é comportamento de spam: link, propaganda, muita mensagem para quem nunca falou com ele, denúncia. Um código pedido pela própria pessoa, curto e sem link, é o caso mais seguro que existe — e o número do SuperDB é o mesmo para todos os projetos. Você escolhe a marca: o nome do projeto, quando pareia o seu número.
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
Login sem senha pelo WhatsApp, com o telefone da conta confirmado: quem digitou o código tem aquele número.
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
// perfis.user_id tem "default auth.uid()": o id vem do token, e o app não manda
const { error } = await db.from('perfis').insert({ nome: 'Fulano', origem: 'landing_b' })
if (error && error.code !== '23505') throw new Error(error.message) // 23505: o perfil já existia
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.
O número do SuperDB tem ainda um teto somando todos os projetos:
60 mensagens por hora e 300 por dia. Acima dele, o pedido responde
503 whatsapp_send_failed sem gerar código, e o app cai para o e-mail como no
Passo 4. É o que protege o número de um script que dispare códigos para números
aleatórios pelos projetos de todo mundo. Com o seu número pareado, esse teto não se
aplica — só os limites de cima.
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; as outras são o número de origem ter sido
desconectado e o número do SuperDB ter chegado ao teto (a mensagem de erro diz qual).
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 Autenticação → WhatsApp do projeto, com o botão de reconectar.
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.
Número próprio: use um dedicado. Pareie um chip só para isso, não o WhatsApp pessoal de ninguém. Se o WhatsApp banir o número por volume ou denúncia, quem perde é a conta inteira — conversas, grupos, tudo. Número novo começa devagar: volume alto no primeiro dia, para gente que nunca falou com ele, é o padrão que o WhatsApp pune.
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 2 minutos sem confirmação, ofereça "não recebi, mandar por e-mail", como no Passo 4, em vez de repetir o envio pelo WhatsApp.
Sobre LGPD: o número de telefone é dado pessoal. Mostre o aviso de privacidade antes do botão de envio, não depois, dizendo que o número será usado para login.