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-Tokenglobal: variávelCLIENT_TOKENou arquivodata/client-token. Vale para todas as instâncias.Client-Tokende conta: definido emPUT /admin/accounts/{accountId}. Só dá acesso às instâncias daquela conta; para as demais, a resposta é 404instância não encontrada.Admin-Token: variávelADMIN_TOKENou arquivodata/admin-token.Partner-Token: variávelPARTNER_TOKENou arquivodata/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, comRetry-After) ou 500. - Rate limit por token (instância, conta ou parceiro):
RATE_LIMIT_RPSrequisiçõ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.