PEPINO ZAPdocs
Comece aqui

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.

Ver como Markdown

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:

  1. Obter a API key
  2. Criar uma instância
  3. Parear via QR Code
  4. Enviar a primeira mensagem
  5. 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"
ModoBase URLPrefixo da keyCobrança
Testehttps://sandbox.pepinozap.com.brpzk_test_grátis, com teto de 100 msgs/dia
Produçãohttps://api.pepinozap.com.brpzk_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:

  1. Cadastre-se (empresa, CNPJ, e-mail e senha) em app.pepinozap.com.br. O cadastro exige confirmação de e-mail.
  2. Confirme o e-mail pelo link enviado. O login só libera depois disso.
  3. 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" }'
CampoTipoObrigatórioDescrição
namestringSimRó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:

  • idguarde 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 em DISCONNECTED. O ciclo completo é DISCONNECTED → CONNECTING → QR_PENDING → CONNECTED; veja Ciclo de vida.
  • phoneNumber / jidnull até 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

StatuscodeQuando ocorre
400BAD_REQUESTname ausente ou body JSON malformado.
401UNAUTHORIZEDKey ausente, inválida ou do modo errado para o host.
402PAYMENT_REQUIREDSó em produção: Projeto sem plano ativo. Assine em Meu plano.
403FORBIDDENProjeto inativo (suspenso).
409CONFLICTCota 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:

  • connectedtrue quando o pareamento foi concluído. Pare o polling nesse ponto.
  • qr — objeto { code, at } enquanto não conectado, ou null quando já conectado. Renderize o campo code como um QR Code (com uma biblioteca de QR) para leitura no aparelho. O code muda a cada ciclo: sempre desenhe o code mais 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.
  • qrdata contém { code, at }; renderize o campo code como QR Code para leitura.
  • statusdata conté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"
  }'
CampoTipoObrigatórioDescrição
tostringSimDestinatário. Aceita E.164 só com dígitos (5511999999999), com + (+5511999999999) ou o JID completo (5511999999999@s.whatsapp.net).
bodystringSimTexto da mensagem.
quotedIdstringNãowaMessageId de uma mensagem para citar (responder).
quotedTextstringNãoTexto da mensagem citada (reconstrói o preview da citação).
quotedFromMebooleanNãotrue 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

StatuscodeQuando ocorre
400BAD_REQUESTto ou body ausente, ou body JSON malformado.
401UNAUTHORIZEDKey ausente, inválida ou do modo errado para o host.
402PAYMENT_REQUIREDSó 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.
403FORBIDDENProjeto inativo.
404NOT_FOUNDInstância não encontrada (não existe, é de outro Projeto, ou é de outro modo — teste/produção).
409CONFLICTInstância em estado terminal BANNED ou ERROR — não dá para enviar.
429RATE_LIMITEDSó 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:

TransporteComo funcionaEntregaIndicado para
WebSocketSeu 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.
WebhookA 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" } }.

StatuscodeQuando ocorre
400BAD_REQUESTBody inválido (campo obrigatório ausente ou JSON malformado).
401UNAUTHORIZEDAPI key ausente, inválida ou do modo errado para o host.
402PAYMENT_REQUIREDSem plano ativo ou sem créditos da API (só em produção). Gate de negócio — não repita; assine/recarregue em Meu plano.
403FORBIDDENProjeto inativo ou sem permissão.
404NOT_FOUNDInstância não encontrada (inexistente, de outro Projeto ou de outro modo).
409CONFLICTCriar instância: cota de números do plano atingida. Enviar: instância BANNED/ERROR.
429RATE_LIMITEDSó no sandbox: teto diário do Projeto atingido (100 msgs/dia, enviadas + recebidas).

Próximos passos

On this page