Quick start
Integração mínima ao PEPINO ZAP, ponta a ponta — da API key ao envio da primeira mensagem e ao recebimento de eventos, com cada request e cada resposta.
O PEPINO ZAP é uma API REST que integra o WhatsApp ao seu sistema. Este guia apresenta o caminho mínimo ponta a ponta para colocar uma integração no ar:
- Obter a API key
- Criar uma instância
- Parear via QR Code
- Enviar a primeira mensagem
- Receber mensagens e status
Cada passo traz o curl (ou o snippet equivalente), a resposta JSON real e os erros que você pode encontrar. Os exemplos usam o ambiente de teste (sandbox); para produção, basta trocar a base URL e a API key (veja abaixo). Não é preciso ler nada antes — mas, se quiser o contexto de autenticação e modos, ele está em Autenticação.
Conceitos em 30 segundos
Três palavras se repetem ao longo do guia:
- Projeto — a unidade que sua API key representa. Instâncias, conversas, webhooks e a assinatura/créditos pertencem todos a um Projeto. Tudo que você cria com uma key vive dentro do Projeto dela.
- Instância — uma conexão de WhatsApp dentro do Projeto (um número pareado). Você pode ter mais de uma, conforme a cota do plano.
- Modo (teste/produção) — uma única conta, dois ambientes isolados, decididos pelo host. O sandbox é grátis (com teto); a produção exige plano ativo. Dados de um modo nunca aparecem no outro.
Base URL e variáveis
Defina a base URL e a API key uma única vez no shell — os exemplos abaixo reaproveitam essas variáveis.
export BASE_URL="https://sandbox.pepinozap.com.br"
export API_KEY="pzk_test_xxxxxxxxxxxxxxxx"| Modo | Base URL | Prefixo da key | Cobrança |
|---|---|---|---|
| Teste | https://sandbox.pepinozap.com.br | pzk_test_ | grátis, com teto de 100 msgs/dia |
| Produção | https://api.pepinozap.com.br | pzk_live_ | exige plano ativo |
O host decide o modo; a key tem que casar com o host. Quem define se a
requisição é teste ou produção é o hostname: sandbox.* é teste, api.* é
produção. A key precisa ter o prefixo do modo do host — pzk_test_ em
sandbox.* e pzk_live_ em api.*. Usar a key do modo errado (por exemplo,
uma pzk_live_ contra o sandbox.*) é rejeitado com 401. Detalhes em
Modos: teste e produção.
Todas as chamadas REST levam o header Authorization: Bearer <API_KEY>. A única exceção é o stream de QR via SSE, que autentica por ?token= (explicado no passo 3) porque o EventSource do navegador não envia headers customizados.
Envelope de erro
Sempre que algo dá errado, a resposta segue o mesmo formato — guarde este shape, ele se repete em todos os passos:
{
"error": {
"code": "UNAUTHORIZED",
"message": "API key ausente ou inválida"
}
}Use o code (estável, para switch no seu código) e exiba a message (legível, em PT-BR) para o usuário/log.
1. Obtenha a API key
A geração de keys é self-service — você não depende de ninguém para começar:
- Cadastre-se (empresa, CNPJ, e-mail e senha) em
app.pepinozap.com.br. O cadastro exige confirmação de e-mail. - Confirme o e-mail pelo link enviado. O login só libera depois disso.
- No painel, vá em Integrações → API Keys. No topo há um toggle Teste / Produção — escolha Teste para seguir este guia e gere a key.
A key crua aparece uma única vez, no momento da criação — copie e guarde em variável de ambiente (não há como recuperá-la depois; se perder, revogue e crie outra). O modo da key é o do toggle no instante em que ela foi gerada: Teste → pzk_test_, Produção → pzk_live_.
O passo a passo completo está em Autenticação.
Confira que a key funciona
Antes de seguir, faça uma chamada de leitura (não exige plano, não consome nada). Listar instâncias de um Projeto novo devolve um array vazio:
curl "$BASE_URL/v1/instances" \
-H "Authorization: Bearer $API_KEY"Resposta 200:
[]Se você receber 401, a key está ausente/inválida ou não casa com o host (veja o Callout acima). Se receber qualquer JSON sem error, a autenticação está ok.
Plano ativo só é exigido em produção, e só para usar de verdade. No modo
teste (sandbox) criar instâncias e enviar mensagens é grátis — você valida
a integração inteira sem assinar. Em produção, criar instância e enviar
exigem um plano ativo no Projeto; sem assinatura, esses endpoints retornam
402 (PAYMENT_REQUIRED). Leitura, QR e marcar como lida ficam sempre
liberados. Ver Plano ativo.
2. Crie uma instância
Uma instância representa uma conexão de WhatsApp dentro do seu Projeto. Criá-la é só registrar o slot — ela nasce desconectada; o pareamento com o número vem no passo 3.
curl -X POST "$BASE_URL/v1/instances" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "minha-instancia" }'| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string | Sim | Rótulo livre para você identificar a instância. Pode repetir — não é único; serve só para você se organizar. |
Resposta 201 Created:
{
"id": "cm5x8a9k20001abc123def456",
"projectId": "cm5x8a9k20000abc123def000",
"name": "minha-instancia",
"status": "DISCONNECTED",
"phoneNumber": null,
"jid": null,
"settings": {
"rejectCalls": false,
"rejectCallMessage": "",
"ignoreGroups": false,
"alwaysOnline": false,
"readReceipts": false
},
"createdAt": "2026-05-29T22:15:00Z",
"updatedAt": "2026-05-29T22:15:00Z"
}Campos da resposta:
id— guarde este valor: ele identifica a instância em todas as próximas chamadas (substitui o{id}nas URLs).projectId— o Projeto ao qual a instância pertence (o da sua key).status— começa sempre emDISCONNECTED. O ciclo completo éDISCONNECTED → CONNECTING → QR_PENDING → CONNECTED; veja Ciclo de vida.phoneNumber/jid—nullaté o pareamento; preenchidos quando a instância conecta.settings— configurações de comportamento (rejeitar chamadas, ignorar grupos etc.), todas desligadas por padrão. Como ajustar: Configurações da instância.
# Guarde o id para reusar nos próximos passos:
export INSTANCE_ID="cm5x8a9k20001abc123def456"Erros possíveis na criação
| Status | code | Quando ocorre |
|---|---|---|
400 | BAD_REQUEST | name ausente ou body JSON malformado. |
401 | UNAUTHORIZED | Key ausente, inválida ou do modo errado para o host. |
402 | PAYMENT_REQUIRED | Só em produção: Projeto sem plano ativo. Assine em Meu plano. |
403 | FORBIDDEN | Projeto inativo (suspenso). |
409 | CONFLICT | Cota de números do plano atingida — contrate mais um número em Meu plano. (Não é nome duplicado; nomes podem repetir.) |
Como desfazer
Para remover a instância (retorna 204 No Content, sem corpo):
curl -X DELETE "$BASE_URL/v1/instances/$INSTANCE_ID" \
-H "Authorization: Bearer $API_KEY"3. Pareie via QR Code
A instância nasceu DISCONNECTED. Para conectá-la a um número, você gera um QR Code e o escaneia no app do WhatsApp do aparelho (Aparelhos conectados → Conectar um aparelho). Há duas formas de obter esse QR.
Recomendamos o polling: cada requisição é curta e atravessa antivírus e proxies corporativos sem problemas. O stream SSE é uma alternativa, porém alguns antivírus e proxies bufferizam streams e impedem a entrega do QR Code. O guia completo (com o vocabulário rico de status do SSE) está em Sua primeira instância.
Polling (recomendado)
Faça GET /v1/instances/{id}/qr/poll a cada ~2 segundos. Diferente do SSE, esse endpoint usa o header Authorization normalmente.
curl "$BASE_URL/v1/instances/$INSTANCE_ID/qr/poll" \
-H "Authorization: Bearer $API_KEY"Enquanto não conectado, a resposta 200 traz o QR atual:
{
"connected": false,
"qr": {
"code": "2@AbCdEf123...,Xyz==,...",
"at": "2026-05-29T22:15:10Z"
}
}Depois que o número escaneia e pareia:
{
"connected": true,
"qr": null
}Como interpretar:
connected—truequando o pareamento foi concluído. Pare o polling nesse ponto.qr— objeto{ code, at }enquanto não conectado, ounullquando já conectado. Renderize o campocodecomo um QR Code (com uma biblioteca de QR) para leitura no aparelho. Ocodemuda a cada ciclo: sempre desenhe ocodemais recente que o poll retornar.
const id = process.env.INSTANCE_ID;
const apiKey = process.env.API_KEY;
const BASE_URL = 'https://sandbox.pepinozap.com.br';
const timer = setInterval(async () => {
const res = await fetch(`${BASE_URL}/v1/instances/${id}/qr/poll`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const { connected, qr } = await res.json();
if (connected) {
clearInterval(timer); // pareamento concluído — pode enviar
return;
}
if (qr) {
renderQrCode(qr.code); // desenhe "qr.code" como QR Code
}
}, 2000);A primeira chamada ao qr/poll (e ao stream SSE) acorda o número: ela pede
para a plataforma conectar a instância na hora e gerar o QR, sem esperar o ciclo
automático. Por isso, na primeiríssima chamada o qr pode vir null por um ou
dois segundos — basta continuar o polling que o code aparece em seguida.
Alternativa: stream SSE
O endpoint GET /v1/instances/{id}/qr transmite Server-Sent Events (Content-Type: text/event-stream). Como o EventSource do navegador não envia o header Authorization, este endpoint autentica pela query string ?token=<API_KEY>:
curl -N "$BASE_URL/v1/instances/$INSTANCE_ID/qr?token=$API_KEY"O stream emite estes eventos:
ready— evento inicial, indicando que o fluxo foi aberto.qr—datacontém{ code, at }; renderize o campocodecomo QR Code para leitura.status—datacontém{ status, jid?, reason?, at? }, refletindo a transição da instância.ping— keepalive emitido periodicamente (a cada ~25s) para manter a conexão viva atrás de proxies.
Pelo SSE, o campo status usa um vocabulário "rico" — maior que o enum
de 6 valores do ciclo de vida. Trate PAIRED/CONNECTED como sucesso e
PAIR_TIMEOUT/PAIR_ERROR/LOGGED_OUT/STREAM_REPLACED/TEMPORARY_BAN como
falha/encerramento. Não faça um switch estrito só nos 6 valores do enum: o
stream pareceria "travado" esperando um CONNECTED que, num timeout/erro, nunca
chega. O polling evita essa complexidade — ele só expõe connected: true/false. A lista completa de status do SSE está em
Sua primeira instância.
Para não expor a API key crua na URL, emita um token efêmero com POST /v1/instances/{id}/qr-token (válido ~2 min, escopado a esta instância) e passe-o no ?token= em vez da key:
curl -X POST "$BASE_URL/v1/instances/$INSTANCE_ID/qr-token" \
-H "Authorization: Bearer $API_KEY"Resposta 200:
{ "token": "eyJhbGciOi..." }Confirme a conexão
Após o pareamento, a instância reconecta sozinha depois de quedas, reinícios ou deploys, sem novo QR — exceto nos estados terminais BANNED e ERROR, ou se o número for deslogado no aparelho (volta a DISCONNECTED, exigindo novo pareamento). Você pode conferir o estado a qualquer momento:
curl "$BASE_URL/v1/instances/$INSTANCE_ID" \
-H "Authorization: Bearer $API_KEY"Com a instância pareada, a resposta 200 agora traz phoneNumber e jid preenchidos:
{
"id": "cm5x8a9k20001abc123def456",
"projectId": "cm5x8a9k20000abc123def000",
"name": "minha-instancia",
"status": "CONNECTED",
"phoneNumber": "5511999999999",
"jid": "5511999999999@s.whatsapp.net",
"settings": { "rejectCalls": false, "rejectCallMessage": "", "ignoreGroups": false, "alwaysOnline": false, "readReceipts": false },
"createdAt": "2026-05-29T22:15:00Z",
"updatedAt": "2026-05-29T22:16:40Z"
}4. Envie a primeira mensagem
Com a instância em CONNECTED, envie uma mensagem de texto com POST /v1/instances/{id}/messages.
Confirme CONNECTED antes de enviar. Enviar para uma instância que ainda
não está conectada também retorna 202 ({ queued: true }), mas a mensagem
não sai — fica em espera indefinida e não há evento de falha. Antes de
enviar, cheque GET /v1/instances/{id} (status === "CONNECTED") ou o
qr/poll (connected === true); e pare de enviar ao receber instance.status
DISCONNECTED/LOGGED_OUT.
curl -X POST "$BASE_URL/v1/instances/$INSTANCE_ID/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "5511999999999",
"body": "Mensagem de teste"
}'| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | Sim | Destinatário. Aceita E.164 só com dígitos (5511999999999), com + (+5511999999999) ou o JID completo (5511999999999@s.whatsapp.net). |
body | string | Sim | Texto da mensagem. |
quotedId | string | Não | waMessageId de uma mensagem para citar (responder). |
quotedText | string | Não | Texto da mensagem citada (reconstrói o preview da citação). |
quotedFromMe | boolean | Não | true se a mensagem citada foi enviada por você. |
Resposta 202 Accepted:
{ "queued": true, "to": "5511999999999" }O envio é assíncrono. O 202 indica apenas que a mensagem foi
enfileirada — não que foi entregue. A confirmação de entrega e de leitura
chega depois pelo evento message.status (próximo passo). O campo to na
resposta ecoa exatamente o que você enviou.
O mesmo fluxo em JavaScript com fetch:
const res = await fetch(`${BASE_URL}/v1/instances/${INSTANCE_ID}/messages`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to: '5511999999999', body: 'Mensagem de teste' }),
});
const data = await res.json(); // { queued: true, to: '5511999999999' }Erros possíveis no envio
| Status | code | Quando ocorre |
|---|---|---|
400 | BAD_REQUEST | to ou body ausente, ou body JSON malformado. |
401 | UNAUTHORIZED | Key ausente, inválida ou do modo errado para o host. |
402 | PAYMENT_REQUIRED | Só em produção: sem plano ativo ou sem créditos da API (modo API). Gate de negócio — não repita; assine/recarregue em Meu plano. |
403 | FORBIDDEN | Projeto inativo. |
404 | NOT_FOUND | Instância não encontrada (não existe, é de outro Projeto, ou é de outro modo — teste/produção). |
409 | CONFLICT | Instância em estado terminal BANNED ou ERROR — não dá para enviar. |
429 | RATE_LIMITED | Só no sandbox: teto diário do Projeto atingido (100 msgs/dia, enviadas + recebidas). |
Texto é só o começo. Os mesmos gates valem para os outros tipos: mídia por URL
(/messages/media), localização (/messages/location), contato
(/messages/contact), figurinha (/messages/sticker), reação
(/messages/reaction) e enquete (/messages/poll). Veja a referência em
Mensagens.
5. Receba mensagens e status
Mensagens recebidas (message.received) e confirmações de entrega/leitura (message.status), além das mudanças de conexão da instância (instance.status), chegam por eventos — não por polling da API. Há dois transportes, que entregam os mesmos eventos, com o mesmo envelope:
| Transporte | Como funciona | Entrega | Indicado para |
|---|---|---|---|
| WebSocket | Seu cliente abre uma conexão e recebe os eventos. | Só enquanto conectado (sem buffer/retry). | Frontends, dashboards, protótipos, quem não tem URL pública. |
| Webhook | A plataforma faz POST assinado no seu endpoint. | Garantida: com retry e backoff (até 6 tentativas). | Integrações de backend que exigem durabilidade. |
Os dois podem ser usados ao mesmo tempo. Ambos são self-service via API key.
Caminho mais rápido para testar: WebSocket
Não precisa de URL pública nem cadastro. Conecte com a sua key na query (o handshake do WebSocket não permite header Authorization) e receba os eventos de todas as instâncias do Projeto:
const ws = new WebSocket(`wss://sandbox.pepinozap.com.br/v1/ws?token=${API_KEY}`);
ws.onmessage = (e) => {
const evt = JSON.parse(e.data);
if (evt.event === 'connected') return; // frame de confirmação inicial
console.log(evt.event, evt.instanceId, evt.data);
};Logo após conectar, o servidor envia { "event": "connected", "projectId": "..." }. Depois, cada evento chega como um frame de texto com o envelope:
{
"event": "message.received",
"instanceId": "cm5x8a9k20001abc123def456",
"timestamp": "2026-05-29T22:16:30Z",
"data": { }
}Detalhes (reconexão com backoff, ciclo de vida) em WebSocket.
Entrega garantida: Webhook
Para backend, cadastre um endpoint via API e receba o secret (whsec_) na criação — guarde-o para validar a assinatura de cada entrega:
curl -X POST "$BASE_URL/v1/webhooks" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "meu-endpoint",
"url": "https://meusistema.com/webhook/pepinozap",
"events": []
}'A plataforma passa a fazer POST no seu url a cada evento, com os headers X-Pepino-Event, X-Pepino-Webhook-Id e X-Pepino-Signature: sha256=<hmac-hex>. Trate as entregas de forma idempotente (o mesmo evento pode chegar mais de uma vez; deduplique pelo X-Pepino-Webhook-Id) e responda 2xx. Veja Webhooks, Eventos e Assinatura.
Tudo aqui é self-service via API key: gerar a key, criar instância, parear,
enviar e receber. Em produção é preciso um plano ativo (senão 402); no
modo teste você testa de graça, com um teto de 100 msgs/dia por Projeto
(enviadas + recebidas; acima disso, 429). Ver
Autenticação.
Erros esperados (resumo)
Os erros seguem o envelope { "error": { "code", "message" } }.
| Status | code | Quando ocorre |
|---|---|---|
400 | BAD_REQUEST | Body inválido (campo obrigatório ausente ou JSON malformado). |
401 | UNAUTHORIZED | API key ausente, inválida ou do modo errado para o host. |
402 | PAYMENT_REQUIRED | Sem plano ativo ou sem créditos da API (só em produção). Gate de negócio — não repita; assine/recarregue em Meu plano. |
403 | FORBIDDEN | Projeto inativo ou sem permissão. |
404 | NOT_FOUND | Instância não encontrada (inexistente, de outro Projeto ou de outro modo). |
409 | CONFLICT | Criar instância: cota de números do plano atingida. Enviar: instância BANNED/ERROR. |
429 | RATE_LIMITED | Só no sandbox: teto diário do Projeto atingido (100 msgs/dia, enviadas + recebidas). |
Próximos passos
- Autenticação — API keys, modos teste/produção e o
402de plano em detalhe. - Sua primeira instância — ciclo de vida, pareamento por QR e configurações de comportamento da instância.
- API Reference: Mensagens — envio de texto, mídia, localização, contato, figurinha, reação e enquete.
- Webhooks: Eventos — o payload de cada evento recebido.
Introdução
API REST do PEPINO ZAP para conectar números de WhatsApp e enviar/receber mensagens — conceitos, modos teste/produção e como começar.
Autenticação
Como criar a conta, gerar e revogar API keys (self-service), os modos teste/produção (host sandbox./api. + prefixo pzk_test_/pzk_live_ que casam), o header Authorization, o fallback ?token= para SSE e todos os erros (401, 402, 403, 429).