Coleção Postman

WL API (zap-api) — referência da API (v0.4, Fases 1 a 3)

Contrato estável para clientes (ex.: Agenda Pro) e para o painel. Mudanças incompatíveis só com nova versão. Rotas, nomes de campos e payloads de webhook seguem a Z-API sempre que possível.

Autenticação

Prefixo Header Quem usa
/instances/{id}/token/{token}/... Client-Token: <token global ou da conta> clientes da API
/admin/... Admin-Token: <token de admin> painel interno
/media/..., /health, /billing/stripe/webhook nenhum (o webhook confere a assinatura do Stripe) público
/instances (GET), /instances/integrator/on-demand..., /instances/{id}/token/{token}/integrator/on-demand... Authorization: Bearer <Partner-Token> parceiros (revenda)
  • Client-Token global: variável CLIENT_TOKEN ou arquivo data/client-token. Vale para todas as instâncias.
  • Client-Token de conta: definido em PUT /admin/accounts/{accountId}. Só dá acesso às instâncias daquela conta; para as demais, a resposta é 404 instância não encontrada.
  • Admin-Token: variável ADMIN_TOKEN ou arquivo data/admin-token.
  • Partner-Token: variável PARTNER_TOKEN ou arquivo data/partner-token.
  • Erros: {"error": "mensagem"} com HTTP 400 (validação), 401 (token), 402 (assinatura sem validade), 403 (IP fora da allowlist), 404 (instância/item), 429 (rate limit, com Retry-After) ou 500.
  • Rate limit por token (instância, conta ou parceiro): RATE_LIMIT_RPS requisições/s, com rajada de 2×.
  • Corpo sempre JSON (Content-Type: application/json), até 150 MB.

Toda rota de instância também existe em /admin/instances/{id}/<rota> (sem o token da instância, com Admin-Token). Exemplo: GET /admin/instances/{id}/qr-code-image.

Nos exemplos:

I=http://localhost:8080/instances/$ID/token/$TOKEN
H='-H "Client-Token: $CT" -H "Content-Type: application/json"'

1. Conexão

Método Rota Resposta
GET qr-code-image {"value":"data:image/png;base64,..."} ou {"connected":true,"value":null}
GET qr-code {"value":"<string do QR>"}
GET phone-code/{telefone} {"value":"ABCD1234"} (código para "conectar com número")
GET status {"connected","session","smartphoneConnected","phone","error"}
POST/GET restart {"value":true} (reconecta a sessão existente)
POST/GET disconnect {"value":true} (desvincula o número; precisa de novo QR)

O QR muda a cada ~20 s; chame de novo para pegar o atual. Após ~2 min sem leitura o pareamento expira e a próxima chamada começa outro.

2. Envio de mensagens

Todas as rotas send-* respondem na hora, antes do envio real:

{"zaapId": "32 hex", "messageId": "3EB0...", "id": "3EB0..."}

O resultado chega depois no webhook delivery (DeliveryCallback, com error se falhou).

Campos comuns: phone (obrigatório: DDI+DDD+número, ex. 5567999999999, ou <id>-group), delayMessage (1–15 s, opcional; substitui o delay da instância), viewOnce (imagem/áudio/vídeo).

Mídia (image, audio, video, document, sticker) aceita URL http(s), base64 puro ou data URI (data:image/png;base64,...). Limite de 100 MB.

Rota Corpo Observações
POST send-text {phone, message}
POST send-image {phone, image, caption?, viewOnce?} gera miniatura automática
POST send-audio {phone, audio, viewOnce?} OGG/Opus vira "áudio gravado" (PTT); MP3/M4A vai como arquivo de áudio
POST send-video {phone, video, caption?, viewOnce?} use MP4 (H.264/AAC)
POST send-document/{extensão} {phone, document, fileName?, caption?} ex.: send-document/pdf; mimetype pela extensão
POST send-link {phone, message, linkUrl, title?, linkDescription?, image?} se message não contém a URL, ela é anexada
POST send-location {phone, latitude, longitude, title?, address?} lat/long aceitam número ou string
POST send-contact {phone, contactName, contactPhone, contactBusinessDescription?}
POST send-sticker {phone, sticker} precisa ser WEBP (ideal 512×512)
curl -s -X POST $I/send-image -H "Client-Token: $CT" -H 'Content-Type: application/json' \
  -d '{"phone":"5567999999999","image":"https://exemplo.com/foto.jpg","caption":"Olá"}'

3. Fila

As mensagens ficam numa fila persistente (SQLite) por instância e saem uma por vez, com delay aleatório entre delayMin e delayMax segundos (padrão 1–3 s). Sobrevive a reinícios. Se a instância cair, a fila espera a reconexão. Uma mensagem que estava sendo enviada no momento de uma queda vira FAILED, para nunca sair duplicada.

Método Rota Resposta
GET queue?page=1&pageSize=100 {"messages":[{_id, ZaapId, MessageId, InstanceId, Phone, Message, Type, DelayMessage, Created}], "total", "page", "pageSize"}
DELETE queue {"value":true,"deleted":N} cancela tudo
DELETE queue/{zaapId ou messageId} {"value":true,"deleted":1} ou 404

Configurações da fila:

Método Rota Corpo
PUT/POST update-message-delay {"delayMin":1,"delayMax":3} (0–60 s)
PUT/POST update-disable-enqueue-when-disconnected {"value":true}: send-* responde 400 se a instância estiver desconectada

4. Webhooks

Método Rota Corpo
PUT/POST update-webhook-received {"value":"https://..."}
PUT/POST update-webhook-delivery idem
PUT/POST update-webhook-message-status idem
PUT/POST update-webhook-connected idem
PUT/POST update-webhook-disconnected idem
PUT/POST update-webhook-chat-presence idem
PUT/POST update-every-webhooks {"value":"https://...","notifySentByMe":true} (aplica a URL em todos)
PUT/POST update-notify-sent-by-me {"notifySentByMe":true}: mensagens enviadas pelo próprio celular também vão para "received" com fromMe:true
PUT/POST update-receive-filters {"ignoreGroups","ignorePrivate","ignoreText","ignoreImage","ignoreVideo","ignoreAudio","ignoreDocument"} (campos omitidos mantêm o valor)

value vazio desliga o webhook. Entrega: POST JSON; se a resposta não for 2xx, tenta de novo em 2, 4 e 8 s.

Payloads

ReceivedCallback (received):

{
  "type": "ReceivedCallback", "instanceId": "...", "messageId": "...",
  "phone": "5567999999999", "fromMe": false, "momment": 1794278655000, "status": "RECEIVED",
  "chatName": "Fulano", "senderName": "Fulano", "senderPhoto": null, "photo": null,
  "senderLid": "123@lid", "connectedPhone": "5567...", "isGroup": false, "participantPhone": null,
  "isNewsletter": false, "isStatusReply": false, "isEdit": false, "broadcast": false,
  "waitingMessage": false, "forwarded": false, "fromApi": false, "messageExpirationSeconds": 0,
  "text": { "message": "Oi!" }
}

Em grupos: phone = <id>-group e participantPhone = quem mandou. Uma chave por tipo:

Tipo Chave e campos
texto text: {message}
link text: {message, title, description, url}
imagem image: {imageUrl, thumbnailUrl, mimeType, caption, width, height, viewOnce, downloadError}
áudio audio: {audioUrl, ptt, seconds, mimeType, viewOnce}
vídeo video: {videoUrl, caption, mimeType, seconds, viewOnce}
documento document: {documentUrl, mimeType, title, pageCount, fileName, caption}
figurinha sticker: {stickerUrl, mimeType}
localização location: {latitude, longitude, name, address, url}
contato contact: {displayName, vCard, phones: []}
reação reaction: {value, time, reactionBy, referencedMessage: {messageId, fromMe, phone}}

As URLs de mídia apontam para PUBLIC_URL/media/<instância>/<arquivo aleatório> (o serviço baixa e guarda a mídia). Em produção, PUBLIC_URL precisa ser acessível pelo cliente.

DeliveryCallback (delivery): {type, instanceId, phone, zaapId, messageId, momment, error?}

MessageStatusCallback (message-status): {type, instanceId, status, ids: [...], phone, phoneDevice: 0, isGroup, momment}. status: SENT (servidor aceitou), RECEIVED (entregue), READ, PLAYED, READ_BY_ME (você leu no celular).

PresenceChatCallback (chat-presence): {type, instanceId, phone, status, lastSeen}; status: AVAILABLE, UNAVAILABLE, COMPOSING, RECORDING, PAUSED.

ConnectedCallback: {type, instanceId, connected: true, phone, momment}

DisconnectedCallback: {type, instanceId, disconnected: true, reason, error, momment}; reason: manual, logged_out, stream_replaced ou temporary_ban.

5. Instância e perfil

Método Rota Corpo / resposta
GET me id, token, name, due, connected, phone, paymentStatus, created, *CallbackUrl, receiveCallbackSentByMe, autoReadMessage, callRejectAuto, callRejectMessage, delayMin, delayMax, disableEnqueueWhenDisconnected, receiveFilters
GET device {phone, imgUrl, about, name, device: {sessionName, device_model}, originalDevice, sessionId, isBusiness}
PUT/POST rename-instance {"value":"Nome"} (nome interno)
PUT/POST profile-name {"value":"Nome no WhatsApp"}
PUT/POST profile-description {"value":"Recado"}
PUT/POST profile-picture {"value":"URL ou base64"}: recorta em quadrado e converte para JPEG 640px; vazio remove a foto. Resposta {value, pictureId}
PUT/POST update-auto-read-message {"value":true}: marca recebidas como lidas
PUT/POST update-call-reject-auto {"value":true}: rejeita chamadas
PUT/POST update-call-reject-message {"value":"texto"}: enviado a quem ligou, se a rejeição estiver ligada

5b. Mensagens avançadas (Fase 2)

Todas entram na fila e respondem {zaapId, messageId, id}. Campos extras aceitos em qualquer send-*:

Campo Efeito
messageId responde (cita) essa mensagem
mentioned: ["5567..."] menciona participantes (o texto deve conter @5567...)
groupMentioned: [{phone, subject}] menciona um grupo
editMessageId (em send-text) edita uma mensagem enviada antes
Rota Corpo
POST forward-message {phone, messageId, messagePhone}: só mensagens que passaram por este serviço
POST send-reaction {phone, messageId, reaction: "👍"}
POST send-remove-reaction {phone, messageId}
POST send-poll {phone, message, poll: [{name}], pollMaxOptions?} (1 = escolha única; 0 = várias)
POST send-poll-vote {phone, pollMessageId, pollVote: [{name}]}
POST send-button-actions {phone, message, buttonActions: [{type: URL|CALL|REPLY, label, url?, phone?, id?}], title?, footer?, image?}
POST send-button-list {phone, message, buttonList: {buttons: [{id?, label}]}, title?, footer?, image?}
POST send-option-list {phone, message, optionList: {title, buttonLabel, options: [{id?, title, description?}]}}
POST send-button-pix {phone, pixKey, type: CPF|CNPJ|PHONE|EMAIL|EVP, merchantName?}
POST send-button-otp {phone, message, code, buttonText?, image?}
POST send-carousel {phone, message, carousel: [{text, image, buttons?: [...como buttonActions]}]}
POST send-ptv {phone, ptv}: vídeo redondo (MP4 por URL ou base64)
POST send-gif {phone, gif, caption?}: MP4 tocado como GIF
POST pin-message {phone, messageId, messageAction: pin|unpin, pinMessageDuration: 24_hours|7_days|30_days}
POST send-event {phone, event: {name, dateTime: "2026-11-20T15:00:00.000Z", timeZone?: "UTC-3", description?, location?: {name}, canceled?}}

Diretas (respondem na hora):

Rota Parâmetros Resposta
DELETE messages query messageId, phone, owner=true|false, deleteForMe=true|false {value:true} (padrão: apaga para todos)
POST read-message {phone, messageId} {value:true}

Mensagens interativas (botões, lista, Pix, OTP, carrossel) usam o formato "native flow" do WhatsApp. Foram testadas e renderizaram num iPhone, mas o WhatsApp pode mudar isso sem aviso.

Webhooks novos (ReceivedCallback): poll: {question, pollMaxOptions, options: [{name}]}, pollVote: {pollMessageId, options: [{name}]} (votos são decifrados e traduzidos para o nome da opção), buttonsResponseMessage: {buttonId, message}, listResponseMessage: {message, title, selectedRowId}, event: {name, description, startTime, canceled, location}.

5c. Contatos

Método Rota Resposta
GET contacts?page=1&pageSize=100 [{phone, name, short, vname, notify, lid}]
POST contacts/add corpo [{firstName, lastName?, phone}] salva na agenda do celular → {success, errors}
DELETE contacts/remove 501: ainda não suportado
GET contacts/{phone} {phone, name, short, notify, vname, imgUrl, about, lid}
GET profile-picture?phone= {link}
GET phone-exists/{phone} {exists, phone, lid}
POST phone-exists-batch corpo {phones: [...]} (até 50.000) → [{exists, inputPhone, outputPhone, lid}]
POST contacts/modify-blocked {phone, action: block|unblock} → {value:true}
POST contacts/{phone}/report 501: ainda não suportado

5d. Conversas

Método Rota Corpo / resposta
GET chats?page=1&pageSize=100 [{phone, name, isGroup, unread, messagesUnread, lastMessageTime, archived, pinned, isMuted, muteEndTime, isMarkedSpam, isGroupAnnouncement, lid}]
GET chats/{phone} idem + profileThumbnail, about
POST modify-chat {phone, action}; action: read, unread, archive, unarchive, pin, unpin, mute, unmute, clear, delete
POST send-chat-expiration {phone, chatExpiration: 24_HOURS|7_DAYS|90_DAYS|OFF}

A lista de conversas vem do histórico recebido no pareamento e das mensagens que passam pelo serviço. Uma instância pareada antes da Fase 2 só mostra as conversas a partir de então.

5e. Grupos

groupId/phone aceitam 120363...-group, 120363... ou 120363...@g.us.

Método Rota Corpo / resposta
GET groups?page=1&pageSize=100 lista no formato de chats + communityId
POST create-group {groupName, phones: [], autoInvite} → {phone, phonesNotAdded, invitationLink}
POST update-group-name {groupId, groupName}
POST update-group-description {groupId, groupDescription}
POST update-group-photo {groupId, groupPhoto} (URL/base64)
POST add-participant / remove-participant {groupId, phones, autoInvite?} → {value, phonesNotProcessed}
POST approve-participant / reject-participant {groupId, phones}
POST add-admin / remove-admin {groupId, phones}
POST leave-group {groupId}
GET group-metadata/{groupId} {phone, subject, description, owner, creation, invitationLink, communityId, adminOnlyMessage, adminOnlySettings, requireAdminApproval, isGroupAnnouncement, participants: [{phone, lid, isAdmin, isSuperAdmin}], subjectTime, subjectOwner}
GET light-group-metadata/{groupId} idem, invitationLink: null
GET group-invitation-metadata?url= metadata pelo link de convite + participantsCount
GET group-invitation-link/{groupId} {phone, invitationLink}
POST redefine-invitation-link/{groupId} novo link
GET accept-invite-group?url= {success, phone}
POST update-group-settings {phone, adminOnlyMessage?, adminOnlySettings?, requireAdminApproval?, adminOnlyAddMember?}

Webhook de eventos de grupo (no webhook received): ReceivedCallback com notification e notificationParameters (telefones afetados), além de phone (grupo), participantPhone (quem fez) e participantLid. Valores de notification: GROUP_PARTICIPANT_ADD, GROUP_PARTICIPANT_INVITE, GROUP_PARTICIPANT_REMOVE, GROUP_PARTICIPANT_LEAVE, GROUP_PARTICIPANT_PROMOTE, GROUP_PARTICIPANT_DEMOTE, GROUP_CHANGE_SUBJECT, GROUP_CHANGE_DESCRIPTION, GROUP_CHANGE_ICON, GROUP_CHANGE_INVITE_LINK (+invitationCode, invitationLink), GROUP_CHANGE_ANNOUNCE, GROUP_CHANGE_RESTRICT, GROUP_SUSPENDED, GROUP_UNSUSPENDED.

5f. Privacidade

Método Rota Corpo
GET privacy (extra) configurações atuais: {lastSeen, photo, description, groupAdd, online, readReceipts, callAdd}
POST privacy/last-seen {visualizationType: ALL|NONE|CONTACTS|CONTACT_BLACKLIST}
POST privacy/photo-visualization idem
POST privacy/description idem
POST privacy/group-add {type: ALL|CONTACTS|CONTACT_BLACKLIST}
POST privacy/online {visualizationType: ALL|SAME_LAST_SEEN}
POST privacy/read-receipts?value=enable|disable
POST privacy/messages-duration?value=days90|days7|hours24|disable duração padrão das novas conversas
GET privacy/disallowed-contacts?type= 501: o WhatsApp recusou a consulta

Respostas: {success: true}. contactsBlacklist (adicionar/remover contatos da lista de exceções) ainda responde 501.

LID

O WhatsApp está trocando telefones por LID (123456@lid). O motor converte LID → telefone sempre que conhece o vínculo, e os webhooks trazem os dois: phone + chatLid (conversa individual), senderLid e, em grupos, participantPhone + participantLid. Em qualquer rota, phone também aceita 123456@lid.

6. API admin (painel)

Header Admin-Token.

Método Rota Descrição
PUT /admin/accounts/{accountId} cria a conta ou troca o Client-Token dela. Corpo {"clientToken":"..."} (mínimo 16 caracteres; vazio = gera um). Resposta {accountId, clientToken}. O token antigo para de valer na hora; 409 se o token já for de outra conta
POST /admin/instances cria. Corpo: {accountId?, name, webhooks: {received, delivery, messageStatus, connected, disconnected, chatPresence}, settings?: {...}}. Resposta 201 = objeto instância
GET /admin/instances?accountId= {total, connected, disconnected, instances: [instância]} (accountId opcional filtra)
GET /admin/instances/{id} instância
DELETE /admin/instances/{id} desvincula o número e apaga sessão, log, métricas e mídias
POST /admin/instances/{id}/regenerate-token {id, token} (o token antigo para de funcionar na hora)
GET /admin/instances/{id}/messages?direction=in|out&status=&type=&limit=50&before=<ms> log, mais recentes primeiro
GET /admin/metrics?accountId=&instanceId=&from=YYYY-MM-DD&to=YYYY-MM-DD métricas (padrão: últimos 30 dias; sem filtros = todas; accountId soma só as instâncias da conta)
* /admin/instances/{id}/<rota de instância> qualquer rota das seções 1–5f
GET /admin/accounts/{accountId} {accountId, clientToken, ipAllowlist, instances}
PUT /admin/accounts/{accountId}/ip-allowlist {"ips": ["200.1.2.3", "10.0.0.0/8", "::1"]}: chamadas feitas com o Client-Token da conta vindas de fora da lista recebem 403. Lista vazia libera tudo; o token global não é afetado. Atrás de proxy, use TRUST_PROXY=1 (lê X-Forwarded-For)
GET /admin/tags?accountId= {tags: [{id, accountId, name, color, createdAt, instances}]}
POST /admin/tags {name, color?, accountId?} → 201 com a etiqueta
PUT /admin/tags/{tagId} {name?, color?}
DELETE /admin/tags/{tagId} remove a etiqueta das instâncias e apaga
PUT /admin/instances/{id}/tags {"tagIds": [...]}: substitui as etiquetas da instância

GET /admin/instances também filtra por ?tagId=, e o objeto instância traz "tags": ["id", ...]. 2FA: o painel já faz TOTP por conta própria, então o motor não guarda campos de 2FA.

Cobrança (assinatura por instância)

Estados (subscription.status): trial (2 dias, TRIAL_DAYS), ativa, pendente (trial vencido sem pagamento), em_cancelamento (continua ativa até periodEnd, sem estorno), cancelada, inadimplente (falha de pagamento ou período vencido há mais de BILLING_GRACE_DAYS). paymentStatus em inglês: TRIAL, ACTIVE, PENDING, CANCELLATION_IN_PROGRESS, CANCELED, OVERDUE. isento (cortesia do admin) aparece como ACTIVE, com due: null e subscription.exempt: true. subscription também traz blocked, blockedReason e blockedAt (bloqueio do admin). POST /admin/instances aceita "exempt": true para a instância já nascer isenta.

Sem assinatura válida, send-* responde 402; depois de BILLING_DISCONNECT_DAYS dias a instância é desconectada (suspended: true) mas a sessão continua salva, e reativar reconecta sem novo QR. Instâncias criadas antes da cobrança existir ficam como ativa, sem vencimento. BILLING_ENFORCE=0 desliga o bloqueio (desenvolvimento).

Método Rota Corpo / resposta
GET /admin/instances/{id}/subscription {status, paymentStatus, canSend, trialEndsAt, periodEnd, statusChangedAt, canceledAt, suspended, due, stripeCustomerId, stripeSubscriptionId}
POST /admin/instances/{id}/subscription/checkout {successUrl, cancelUrl} → {url} do Stripe Checkout (exige STRIPE_SECRET_KEY e STRIPE_PRICE_ID)
POST /admin/instances/{id}/subscription/activate {days?: 30} ou {periodEnd: RFC3339}: pagamento manual (Pix, boleto)
POST /admin/instances/{id}/subscription/cancel trial/pendente → cancelada; paga → em_cancelamento até o fim do período (no Stripe: cancel_at_period_end)
POST /admin/instances/{id}/subscription/reactivate desfaz um em_cancelamento
POST /admin/instances/{id}/subscription/set-status {status}: ajuste manual
POST /admin/instances/{id}/subscription/exempt cortesia: status isento (ativa para sempre, sem vencimento; a cobrança e o Stripe não mexem nela)
POST /admin/instances/{id}/subscription/unexempt tira a cortesia. {status?} (padrão pendente; aceita trial, ativa = +30 dias, cancelada, inadimplente)
POST /admin/instances/{id}/subscription/block {reason?}: bloqueio do admin da plataforma. Desconecta, não gera QR e send-* responde 403, mesmo com BILLING_ENFORCE=0
POST /admin/instances/{id}/subscription/unblock desfaz o bloqueio (reconecta se a assinatura permitir)
POST /admin/accounts/{accountId}/{exempt|unexempt|block|unblock} mesma ação em todas as instâncias da conta → {accountId, action, instances: [{id, status, blocked, error?}]}
POST /billing/stripe/webhook webhook do Stripe (configure no painel do Stripe; segredo em STRIPE_WEBHOOK_SECRET)

Eventos do Stripe tratados: checkout.session.completed → ativa; invoice.paid / invoice.payment_succeeded → ativa + periodEnd; invoice.payment_failed → inadimplente; customer.subscription.updated (com cancel_at_period_end) → em_cancelamento / ativa; customer.subscription.deleted → cancelada. A instância é localizada por metadata.instance_id (o checkout já envia), pelo id da assinatura ou pelo cliente. Uma assinatura Stripe por instância.

O objeto instância do admin também traz subscription, paymentStatus, due, proxy e remote (true quando a instância roda em outro servidor do cluster).

Objeto instância:

{
  "id": "32 hex", "token": "24 hex", "name": "Agenda Pro", "createdAt": "2026-11-10T02:12:54Z",
  "accountId": "conta-a", "tags": ["f76e2a5d719fbc1d"],
  "connected": true, "session": true, "phone": "5567...", "error": "",
  "lastPhone": "5567...", "lastDisconnectedAt": "2026-11-10T03:00:00Z", "lastDisconnectReason": "logged_out",
  "webhooks": {"received": "", "delivery": "", "messageStatus": "", "connected": "", "disconnected": "", "chatPresence": ""},
  "settings": {"notifySentByMe": false, "autoReadMessage": false, "callRejectAuto": false, "callRejectMessage": "",
               "delayMin": 1, "delayMax": 3, "disableEnqueueWhenDisconnected": false,
               "receiveFilters": {"ignoreGroups": false, "ignorePrivate": false, "ignoreText": false,
                                  "ignoreImage": false, "ignoreVideo": false, "ignoreAudio": false, "ignoreDocument": false}}
}

lastPhone é o último número que conectou (continua preenchido depois de desconectar). lastDisconnectedAt (RFC3339 ou null) e lastDisconnectReason (manual, logged_out, stream_replaced, temporary_ban) registram a última desconexão. Quedas momentâneas de rede, em que o motor reconecta sozinho, não contam.

Log de mensagem (messages[]): {zaapId, instanceId, messageId, direction: "in"|"out", type, phone, fromMe, status, error?, createdAt, sentAt?, deliveredAt?, readAt?} (tempos em ms). status de saída: QUEUED, SENDING, SENT, RECEIVED, READ, PLAYED, FAILED, CANCELED. Mensagens enviadas pelo próprio celular entram como direction: "out", fromMe: true.

Métricas:

{
  "from": "2026-10-11", "to": "2026-11-09", "instanceId": "",
  "totals": {"sent": 11, "received": 6},
  "byType": {"sent": {"text": 3, "image": 1, ...}, "received": {"text": 0, ...}},
  "daily": [{"day": "2026-11-09", "sent": 11, "received": 6}],
  "rows": [{"instanceId", "day", "direction": "sent|received", "type", "count"}]
}

type ∈ text, image, audio, video, document, link, contact, location, sticker. daily traz todos os dias do período (zeros incluídos) para o gráfico. Dia no fuso do servidor.

Compatibilidade: POST /instances com Client-Token também cria instância (mesmo corpo e resposta). Com o Client-Token de uma conta, a instância fica nessa conta.

7. Partner API (revenda)

Header Authorization: Bearer <Partner-Token>; não usa Client-Token. A Z-API não publica os caminhos de todas estas rotas; os abaixo seguem o padrão integrator/on-demand dela.

Método Rota Corpo Resposta
POST /instances/integrator/on-demand {name, receivedCallbackUrl?, deliveryCallbackUrl?, receivedAndDeliveryCallbackUrl?, messageStatusCallbackUrl?, connectedCallbackUrl?, disconnectedCallbackUrl?, presenceChatCallbackUrl?, callRejectAuto?, callRejectMessage?, autoReadMessage?, disableEnqueueWhenDisconnected?} {id, token, due} (trial de 2 dias)
POST /instances/{id}/token/{token}/integrator/on-demand/subscription {withCalls?} 201: assina (ciclo mensal até o fim do mês)
POST /instances/{id}/token/{token}/integrator/on-demand/cancel — 201: em_cancelamento até o fim do ciclo (trial/pendente: cancela na hora)
POST /instances/{id}/token/{token}/integrator/on-demand/update {withCalls: true} 201
PUT /instances/{id}/token/{token}/integrator/on-demand/proxy {proxyUrl: "socks5://user:pass@host:1080", enable: true} {value: true}; 400 Proxy is not reachable
PUT /instances/{id}/token/{token}/integrator/on-demand/update-webhook-proxy {value: url} {value: true}
DELETE /instances/{id}/token/{token}/integrator/on-demand — 204; 403 {type, message}
POST /instances/integrator/on-demand/delete-instances {instances: [{id, token}]} (máx. 100) {total, deleted, rejected, results: [{id, status, type, reason}]}
GET /instances?page=1&pageSize=15&query=&middleware=web — {total, totalPage, pageSize, page, content: [{id, token, name, tenant, created, due, paymentStatus, subscriptionStatus, deliveryCallbackUrl, phoneConnected, whatsappConnected, middleware}]}

Exclusão só para instâncias desconectadas e com assinatura trial, pendente (sem assinatura Stripe aberta) ou cancelada. Tipos de recusa: INSTANCE_STILL_CONNECTED, INSTANCE_NOT_ELIGIBLE, INSTANCE_HAS_ACTIVE_SUBSCRIPTION, INSTANCE_PAYMENT_STATUS_MISSING, INSTANCE_NOT_FOUND, INTERNAL_ERROR.

Proxy: HTTP, HTTPS ou SOCKS5 por instância. Se o proxy não responder em 3 tentativas, a instância conecta sem proxy e o webhook de proxy recebe {type: "ProxyCallback", instanceId, error}. me mostra proxyUrl e useProxy.

8. Vários servidores (cluster)

Com Postgres, vários servidores podem rodar juntos. Cada instância roda em um só servidor (advisory lock no Postgres); quem recebe uma chamada de instância de outro servidor repassa ao dono (SERVER_URL). Se um servidor cai, outro assume as instâncias dele em até ~15 s, e as sessões continuam sem novo QR. Listas (/admin/instances, GET /instances) podem levar até 15 s para refletir mudanças feitas em outro servidor.