Pular para o conteúdo
📖 COOKBOOK

WhatsApp OTP em 8 linhas.

O código de login chega pelo WhatsApp, sem conta na Meta e sem template para aprovar: você pareia o WhatsApp do seu negócio (por QR code ou código de pareamento), ou usa o número do SuperDB. A receita mostra o envio, a confirmação, os limites e como cair para o e-mail quando 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 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_plan e nenhum código é enviado.
  • @superdb/client 0.2.2 ou mais novo, com o cliente criado com o project.
  • 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):

app/login/whatsapp-form.tsx
'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:

A mensagem que o usuário recebe
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:

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

Login sem senha pelo WhatsApp, com o telefone da conta confirmado: quem digitou o código tem aquele número.

WhatsApp · +55 11 4002-8922
v0.4.2
Meu App: seu código de acesso é 478291. Não compartilhe com ninguém. Vale 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

// 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.

Essa página ajudou?