# Quick start (/docs/get-started/quick-start)





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](#1-obtenha-a-api-key)
2. [Criar uma instância](#2-crie-uma-instancia)
3. [Parear via QR Code](#3-pareie-via-qr-code)
4. [Enviar a primeira mensagem](#4-envie-a-primeira-mensagem)
5. [Receber mensagens e status](#5-receba-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](/docs/get-started/authentication).

## Conceitos em 30 segundos [#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 [#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.

```bash
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                |

<Callout type="warn">
  **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.*&#x60;) é rejeitado com &#x2A;*`401`**. Detalhes em
  [Modos: teste e produção](/docs/get-started/authentication#modos-teste-e-producao).
</Callout>

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 [#envelope-de-erro]

Sempre que algo dá errado, a resposta segue o mesmo formato — guarde este shape, ele se repete em todos os passos:

```json
{
  "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 [#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](/docs/get-started/authentication).

### Confira que a key funciona [#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:

```bash
curl "$BASE_URL/v1/instances" \
  -H "Authorization: Bearer $API_KEY"
```

Resposta `200`:

```json
[]
```

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.

<Callout type="info">
  **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](/docs/get-started/authentication#plano-ativo-erro-402).
</Callout>

## 2. Crie uma instância [#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.

```bash
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`:

```json
{
  "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 em `DISCONNECTED`. O ciclo completo é `DISCONNECTED → CONNECTING → QR_PENDING → CONNECTED`; veja [Ciclo de vida](/docs/get-started/first-instance#ciclo-de-vida).
* `phoneNumber` / `jid` — `null` 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](/docs/get-started/first-instance#configuracoes-da-instancia).

```bash
# Guarde o id para reusar nos próximos passos:
export INSTANCE_ID="cm5x8a9k20001abc123def456"
```

### Erros possíveis na criação [#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 [#como-desfazer]

Para remover a instância (retorna `204 No Content`, sem corpo):

```bash
curl -X DELETE "$BASE_URL/v1/instances/$INSTANCE_ID" \
  -H "Authorization: Bearer $API_KEY"
```

## 3. Pareie via QR Code [#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](/docs/get-started/first-instance#parear-via-qr-code).

### Polling (recomendado) [#polling-recomendado]

Faça `GET /v1/instances/{id}/qr/poll` a cada \~2 segundos. Diferente do SSE, esse endpoint usa o header `Authorization` normalmente.

```bash
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:

```json
{
  "connected": false,
  "qr": {
    "code": "2@AbCdEf123...,Xyz==,...",
    "at": "2026-05-29T22:15:10Z"
  }
}
```

Depois que o número escaneia e pareia:

```json
{
  "connected": true,
  "qr": null
}
```

Como interpretar:

* `connected` — `true` quando o pareamento foi concluído. **Pare o polling** nesse ponto.
* `qr` — objeto `{ code, at }` enquanto não conectado, ou `null&#x60; quando já conectado. Renderize o campo &#x2A;*`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.

```javascript
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);
```

<Callout type="info">
  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.
</Callout>

### Alternativa: stream SSE [#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>`:

```bash
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` — `data` contém `{ code, at }`; renderize o campo `code` como QR Code para leitura.
* `status` — `data` 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.

<Callout type="warn">
  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](/docs/get-started/first-instance#alternativa-stream-sse).
</Callout>

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:

```bash
curl -X POST "$BASE_URL/v1/instances/$INSTANCE_ID/qr-token" \
  -H "Authorization: Bearer $API_KEY"
```

Resposta `200`:

```json
{ "token": "eyJhbGciOi..." }
```

### Confirme a conexão [#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:

```bash
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:

```json
{
  "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 [#4-envie-a-primeira-mensagem]

Com a instância em `CONNECTED`, envie uma mensagem de texto com `POST /v1/instances/{id}/messages`.

<Callout type="warn">
  **Confirme `CONNECTED` antes de enviar.** Enviar para uma instância que ainda
  não está conectada &#x2A;*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`.
</Callout>

```bash
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`:

```json
{ "queued": true, "to": "5511999999999" }
```

<Callout type="info">
  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.
</Callout>

O mesmo fluxo em JavaScript com `fetch`:

```javascript
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 [#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).                                                     |

<Callout type="info">
  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](/docs/api/mensagens/sendTextMessage).
</Callout>

## 5. Receba mensagens e status [#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 [#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:

```javascript
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:

```json
{
  "event": "message.received",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-05-29T22:16:30Z",
  "data": { }
}
```

Detalhes (reconexão com backoff, ciclo de vida) em [WebSocket](/docs/webhooks/websocket).

### Entrega garantida: Webhook [#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:

```bash
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](/docs/webhooks), [Eventos](/docs/webhooks/events) e [Assinatura](/docs/webhooks/signing).

<Callout type="info">
  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](/docs/get-started/authentication#modos-teste-e-producao).
</Callout>

## Erros esperados (resumo) [#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 [#próximos-passos]

* [Autenticação](/docs/get-started/authentication) — API keys, modos teste/produção e o `402` de plano em detalhe.
* [Sua primeira instância](/docs/get-started/first-instance) — ciclo de vida, pareamento por QR e configurações de comportamento da instância.
* [API Reference: Mensagens](/docs/api/mensagens/sendTextMessage) — envio de texto, mídia, localização, contato, figurinha, reação e enquete.
* [Webhooks: Eventos](/docs/webhooks/events) — o payload de cada evento recebido.
