---
title: "Onboarding de WhatsApp no seu SaaS com código de pareamento (sem QR, com a sua marca)"
description: "Conecte o WhatsApp do cliente dentro do seu SaaS, com a sua marca e sem QR Code: fluxo, estados da tela, backend em Node.js e webhook de conexão."
url: "https://wame.api.br/blog/onboarding-saas-whatsapp-codigo-pareamento"
language: "pt-BR"
og:type: "article"
published_time: "2026-09-28"
modified_time: "2026-09-28"
author: "Raphael Serafim"
keywords: "api whatsapp não oficial, api não oficial whatsapp, código de pareamento whatsapp, onboarding whatsapp saas, pairing code whatsapp api, conectar whatsapp do cliente api, whatsapp white label api"
reading_time: "9 min de leitura"
---

# Onboarding de WhatsApp no seu SaaS com código de pareamento (sem QR, com a sua marca)

> Conecte o WhatsApp do cliente dentro do seu SaaS, com a sua marca e sem QR Code: fluxo, estados da tela, backend em Node.js e webhook de conexão.

**Para conectar o WhatsApp do cliente dentro do seu SaaS sem QR Code, seu backend chama `POST /{key}/instance/pairing-code` com o número do cliente, recebe o código de 8 caracteres em `code` e mostra na sua tela; o cliente digita no próprio celular e o webhook de conexão avisa quando terminou.** Tudo acontece com a sua marca, na sua interface, e funciona até quando o cliente está com o celular como único aparelho.

Este guia é para quem desenvolve o produto: estados da tela, backend em Node.js, confirmação por webhook e as métricas do funil. A visão geral dos métodos de conexão está em [conectar WhatsApp na API sem QR Code](/blog/conectar-whatsapp-codigo-pareamento-sem-qr-code), e o passo a passo para o usuário final está em [conectar o WhatsApp só pelo celular](/blog/conectar-whatsapp-api-pelo-celular-sem-qr-code).

> A camada não oficial da WAME é independente e não é afiliada, endossada ou suportada pelo WhatsApp ou pela Meta. O uso é de responsabilidade de quem envia.

## Por que código em vez de QR no onboarding

O QR Code tem um problema estrutural para SaaS: ele exige **duas telas**. O cliente precisa ver o QR no computador e ler com o celular. Quem faz o cadastro pelo celular (e no pequeno negócio isso é muito comum) trava ali: não tem como o celular ler um QR exibido nele mesmo.

O código de pareamento resolve isso e ainda traz outras vantagens:

- **Funciona no mesmo aparelho.** O cliente copia o código e cola no WhatsApp.
- **Não depende de câmera**, de enquadramento ou de o QR não expirar enquanto a pessoa procura o celular.
- **Funciona remoto.** O suporte consegue guiar por telefone: "abra Aparelhos conectados e digite o código que aparece na tela".
- **É white label.** O cliente vê a sua tela e as suas instruções.

Em produto, isso vira métrica: menos gente abandonando o onboarding e menos chamados do tipo "não consigo ler o QR".

## Os estados da tela

Antes do código, desenhe a máquina de estados. É ela que evita tela travada e cliente perdido.

| Estado | O que a tela mostra | Próximo |
| --- | --- | --- |
| `pedir_numero` | Campo de telefone com DDI | `gerando` |
| `gerando` | Carregando | `mostrando_codigo`, `conectado` ou `erro` |
| `mostrando_codigo` | Código grande, botão copiar, instruções | `conectado` ou `expirado` |
| `expirado` | "O código expirou" e botão para gerar outro | `gerando` |
| `conectado` | Confirmação e próximo passo do onboarding | fim |
| `erro` | Mensagem clara e opção de tentar de novo | `pedir_numero` |

Três detalhes de UX que fazem diferença:

1. **Mostre o código grande**, em blocos (`XXXX-XXXX`), com botão **Copiar**. É assim que o painel da própria WAME faz.
2. **Instruções ao lado do código**, e não em outra página: "WhatsApp → Aparelhos conectados → Conectar um aparelho → Conectar com número de telefone".
3. **Ofereça o QR como alternativa**, com um link discreto ("Prefere ler um QR Code?"), para quem está no computador.

## Normalizando o número

O `phoneNumber` precisa estar no formato internacional, só dígitos, e ser **o número do celular onde o código será digitado**. Normalize no backend, nunca confie no que veio do formulário:

```js
function normalizarTelefone(entrada, ddiPadrao = '55') {
  let d = String(entrada).replace(/\D/g, '');
  if (d.startsWith('00')) d = d.slice(2);
  // Número brasileiro sem DDI: 10 ou 11 dígitos (DDD + número)
  if (d.length === 10 || d.length === 11) d = ddiPadrao + d;
  if (d.length < 12 || d.length > 15) return null;
  return d;
}
```

No front, deixe claro que é o número do WhatsApp que será conectado, e não um telefone de contato qualquer. É a causa mais comum de "o código não funciona".

## O backend: gerar o código

A key da instância autentica todas as chamadas. **Ela nunca vai para o navegador.** O front chama o seu backend, que sabe qual instância pertence ao cliente logado:

```js
import express from 'express';

const app = express();
app.use(express.json());
const BASE = 'https://server.wame.api.br';

// Cliente autenticado pede o código para o próprio número
app.post('/api/whatsapp/codigo', exigirLogin, async (req, res) => {
  const cliente = await db.clientes.buscar(req.user.id);
  const telefone = normalizarTelefone(req.body.telefone);
  if (!telefone) return res.status(400).json({ erro: 'telefone_invalido' });

  const r = await fetch(`${BASE}/${cliente.instanceKey}/instance/pairing-code`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phoneNumber: telefone }),
  });
  const dados = await r.json();

  if (!r.ok) {
    await metricas.registrar('onboarding_erro', { cliente: cliente.id, status: r.status });
    return res.status(502).json({ erro: 'falha_ao_gerar' });
  }

  // Instância já pareada: não vem código, e está tudo certo
  if (dados.isConnected) {
    await db.clientes.marcarConectado(cliente.id);
    return res.json({ estado: 'conectado' });
  }

  await db.clientes.atualizar(cliente.id, {
    whatsappStatus: 'aguardando_codigo',
    codigoGeradoEm: new Date(),
  });
  await metricas.registrar('onboarding_codigo_gerado', { cliente: cliente.id });

  res.json({ estado: 'mostrando_codigo', codigo: dados.code });
});
```

Repare no `isConnected`. A API não gera código para uma sessão que já está pareada, porque pedir código numa sessão ativa derrubaria a conexão existente. Para o seu produto, isso é sucesso: pule direto para o próximo passo.

Se a sua plataforma cria instâncias por cliente, o `instanceKey` vem do provisionamento. O ciclo de criar, ativar, suspender e remover está em [provisionar WhatsApp para vários clientes por API](/blog/provisionar-whatsapp-multiplos-clientes-api).

Um caso à parte: instâncias criadas para registro mobile, sem aparelho vinculado, não usam código de pareamento e respondem com erro `400`. Esse fluxo é outro, descrito em [WhatsApp sem celular](/blog/whatsapp-sem-celular-conexao-mobile-api).

## Confirmando a conexão pelo webhook

Configure o webhook de conexão da instância uma vez, no provisionamento:

```bash
curl -X PUT "https://server.wame.api.br/SUA_KEY/instance" \
  -H "Content-Type: application/json" \
  -d '{
    "allowWebhook": true,
    "allowNumber": "all",
    "webhookConnection": "https://seusaas.com.br/wame/conexao",
    "webhookFormat": "meta"
  }'
```

Quando o cliente digita o código e o pareamento conclui, chega um evento com `field: "connection"` e `value.connection.status` igual a `"open"`. O campo `instance`, no topo do envelope, traz a key da instância que conectou:

```js
app.post('/wame/conexao', async (req, res) => {
  res.sendStatus(200); // responda rápido; processe depois

  const entry = req.body?.entry?.[0];
  const change = entry?.changes?.[0];
  if (change?.field !== 'connection') return;

  const cliente = await db.clientes.porInstancia(req.body.instance);
  if (!cliente) return;

  if (change.value?.connection?.status === 'open') {
    await db.clientes.marcarConectado(cliente.id);
    await metricas.registrar('onboarding_conectado', { cliente: cliente.id });
    avisarFront(cliente.id, { estado: 'conectado' });
  }
});
```

Proteja esse endpoint como qualquer webhook: URL difícil de adivinhar, validação de origem e idempotência. Os cuidados estão em [segurança da API do WhatsApp: token, webhook e dados](/blog/seguranca-api-whatsapp-token-webhook).

## Levando o status até a tela

O webhook chega no backend; a tela do cliente precisa saber. Duas opções simples:

**Server-Sent Events**, que empurra o status assim que ele muda:

```js
const conexoes = new Map(); // clienteId -> res

app.get('/api/whatsapp/status', exigirLogin, (req, res) => {
  res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
  res.flushHeaders();
  conexoes.set(req.user.id, res);
  req.on('close', () => conexoes.delete(req.user.id));
});

function avisarFront(clienteId, evento) {
  conexoes.get(clienteId)?.write(`data: ${JSON.stringify(evento)}\n\n`);
}
```

No front, poucas linhas:

```js
const fonte = new EventSource('/api/whatsapp/status');
fonte.onmessage = (e) => {
  const { estado } = JSON.parse(e.data);
  if (estado === 'conectado') mostrarTela('conectado');
};
```

**Polling**, se a sua infraestrutura não mantém conexões abertas: o front consulta o seu backend a cada poucos segundos, e o backend responde com o status do banco.

Nos dois casos, tenha uma **rede de segurança**. Se o webhook falhar ou atrasar, o backend consulta `GET /{key}/instance` para ver o estado real antes de declarar o código como expirado.

## Expiração e "gerar outro"

O código vale por pouco tempo, na casa de poucos minutos, e é de uso único. Não tente adivinhar o momento exato da expiração. Use um tempo de tela conservador:

```js
const TEMPO_TELA_MS = 2 * 60 * 1000; // ajuste ao que você observar

setTimeout(async () => {
  const { estado } = await fetch('/api/whatsapp/estado').then((r) => r.json());
  if (estado !== 'conectado') mostrarTela('expirado');
}, TEMPO_TELA_MS);
```

Na tela `expirado`, um botão **Gerar novo código** volta para `gerando`. Limite quantas vezes por minuto o mesmo cliente pode pedir código: além de evitar abuso do seu endpoint, isso impede que um script com erro fique gerando códigos em loop.

## Segurança: o código é uma senha

O código de pareamento vincula um aparelho à conta do cliente. Trate como senha:

- **Mostre só em tela autenticada**, para o dono da conta.
- **Não envie o código** por e-mail, SMS ou chat de suporte. O cliente gera e digita do lado dele.
- **Não registre o código em log** nem em ferramenta de analytics.
- **Eduque o cliente:** "nunca informe esse código a ninguém, nem ao nosso suporte". Isso também protege contra o golpe clássico em que alguém pede "o código que chegou no seu WhatsApp".

## Medindo o funil

Com os eventos acima, você tem o funil de onboarding do WhatsApp:

| Etapa | Evento |
| --- | --- |
| Iniciou | Abriu a tela de conexão |
| Gerou código | `onboarding_codigo_gerado` |
| Conectou | `onboarding_conectado` |
| Expirou sem conectar | Tela `expirado` exibida |
| Erro | `onboarding_erro` |

Acompanhe o tempo entre gerar e conectar, a taxa de expiração e quantos precisaram de um segundo código. Taxa de expiração alta quase sempre é instrução pouco clara ou número errado. Melhore o texto da tela antes de mexer em qualquer outra coisa.

## Depois do onboarding

Conectado, o cliente começa a usar o seu produto. Se o número for novo, oriente sobre ritmo nas primeiras semanas: número recém-conectado que dispara volume alto chama atenção do anti-spam. A WAME não apoia spam, e a taxa de bloqueio é muito baixa para quem usa do jeito certo: atendimento, notificações para quem pediu, grupos e bots. O plano está em [como aquecer um número novo](/blog/aquecer-numero-whatsapp-api-nao-oficial).

Também vale ligar o webhook de conexão ao seu monitoramento, e não só ao onboarding. Quedas depois de conectado são sinal a acompanhar, como mostra [sinais de que o número está em risco](/blog/sinais-numero-whatsapp-risco-bloqueio).

## Conclusão

Onboarding de WhatsApp com código de pareamento é o fluxo que o seu cliente consegue concluir sozinho, pelo celular, sem QR e sem chamar o suporte. A receita: normalize o número no backend, chame `POST /{key}/instance/pairing-code` com a key do cliente (nunca no navegador), mostre o `code` grande com botão de copiar, trate `isConnected` como sucesso e confirme pelo webhook de conexão com `status: "open"`. Adicione a tela de código expirado, meça o funil e trate o código como senha. O resultado é um onboarding white label, com a sua marca, e menos gente parada no primeiro passo. A referência completa dos endpoints está na [documentação](/docs).

## Perguntas frequentes

### Como conectar o WhatsApp do meu cliente sem mandar QR Code?

Na tela de onboarding do seu sistema, o cliente informa o número. Seu backend chama POST /{key}/instance/pairing-code com o phoneNumber, recebe o campo code e mostra os 8 caracteres na tela. O cliente digita o código no próprio celular, em Aparelhos conectados > Conectar com número de telefone. O webhook de conexão avisa quando terminou.

### Posso chamar a API de pareamento direto do navegador?

Não deve. A key da instância autentica todas as chamadas e não pode ficar exposta no front. O navegador chama o seu backend, que conhece a key do cliente e fala com a API. O front só recebe o código e o status.

### Como sei que o cliente terminou de conectar?

Configure o webhookConnection pelo PUT /{key}/instance. Quando o pareamento conclui, chega um evento com field connection e status open. Seu backend atualiza o banco e avisa o front por Server-Sent Events, WebSocket ou polling. Como rede de segurança, consulte GET /{key}/instance.

### E se a instância já estiver conectada?

A resposta do pairing-code vem com isConnected true e sem código. Trate como sucesso e pule para o próximo passo do onboarding. A API não gera código para sessão já pareada, porque isso derrubaria a conexão existente.

### O cliente vai ver a marca WAME no onboarding?

Não precisa. A tela, as instruções e o código aparecem dentro do seu produto. No celular do cliente, a conexão aparece na lista de Aparelhos conectados como uma sessão de navegador. É um fluxo white label.
