7. Mensagens#
Todas as operações assincronas desta seção (send, forward, delete, edit) são processadas em fila. A API retorna 202 Accepted com o message_id do Catcher (UUID pre-gerado na aceitacao da request), idempotency_key e task_id para rastreamento e deduplicacao segura de retries do cliente.
Header obrigatório: Idempotency-Key#
Todos os endpoints assincronos de mensagens exigem o header HTTP Idempotency-Key.
- Use um valor único por operação lógica.
- Repetir a mesma combinação
instanceId+ tipo de operação +Idempotency-Keynão cria uma nova task. - O retry recebe o estado atual do mesmo request (
queued,sentoufailed) com o mesmomessage_ide o mesmotask_id. - Clientes browser podem enviar esse header diretamente: ele faz parte dos headers permitidos no preflight CORS junto com
Authorization,Content-Type,X-API-Key,X-CSRF-Token,X-Force-SendeLast-Event-ID.
Formato de destinatário#
O campo to aceita:
- Número de telefone:
554137984905(será convertido para JID automaticamente) - JID individual:
554137984905@s.whatsapp.net - JID de grupo:
120363012345678901@g.us
Resposta padrão (202)#
Todas as operações que criam uma nova mensagem retornam:
{
"status": "queued",
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"task_id": "asynq:task:xxxx-xxxx",
"idempotency_key": "msg-2026-03-21-0001"
}
message_id— UUID do Catcher, pre-gerado no momento da aceitacao. E o mesmo valor que aparece como elemento demessage_idsnos webhooksmessage.sent,message.delivered,message.read,message.played,message.deleted,message.edited. Use este campo para correlacionar a resposta sincrona com os eventos assincronos (no consumer,message_ids[0]=message_idda resposta 202).idempotency_key— eco do header enviado pelo cliente. Também aparece no webhookmessage.sent(campoidempotency_key) quando a mensagem foi originada via API.task_id— identificador interno do Asynq. Util para debug/NOC; não deve ser usado como chave de correlação primaria (usemessage_idouidempotency_key).
Operações que não criam um novo Message (delete, edit) retornam apenas status, task_id e idempotency_key; o cliente já possui o message_id alvo da operação.
Repetir a mesma chave idempotente devolve o mesmo
message_id,task_ide o estado atual (queued,sentoufailed) sem enfileirar duplicidade. Este e o caminho oficial para um cliente recuperar omessage_idcaso tenha perdido a resposta original ou precise verificar o estado antes do primeiro webhook.
Confirmando entrega de verdade (o 202 não é entrega)#
O 202 significa apenas "aceito na fila". NÃO significa que a mensagem saiu, chegou, ou renderizou no destinatário. Para confirmar de verdade, acompanhe a escada de eventos (webhook/SSE/WS) correlacionando por message_id:
| Evento | Prova | NÃO prova |
|---|---|---|
202 queued (resposta HTTP) |
a task entrou na fila | que saiu / chegou |
message.sent |
o worker despachou pro WhatsApp; a Meta aceitou o proto | que chegou no aparelho |
message.delivered |
chegou no aparelho do destinatário | que renderizou / tocou |
message.read |
o destinatário abriu e o chat renderizou (texto, imagem, documento, sticker, localização, contato, link-preview) | — |
message.played |
o destinatário tocou a mídia (áudio/PTT, vídeo, PTV, view-once) | — |
O salto crítico é delivered → read/played: mídia pode entregar e falhar em renderizar/tocar no cliente (proto incompleto, container errado). Se um envio chega em delivered mas nunca dá read/played, trate como não consumido — não como entregue. Regra prática: "sem read/played, não chegou de verdade."
Pré-condição importante: o read/played só dispara se o destinatário tiver recibos de leitura ligados E efetivamente abrir/tocar a mensagem. Se o destinatário não produz recibo, o piso de verificação é message.delivered (chegou no aparelho) + inspecionar o message.received do lado dele e conferir que o conteúdo bate (==) com o enviado: mesma caption/content, mesmo message_type, media_id presente, phone correto. Isso prova que chegou correto, mesmo sem o sinal de leitura.
Status / Stories têm semântica de recepção diferente: o evento
status.viewed(alguém viu MEU status) só vem de contatos no público do status (contatos salvos filtrados pela privacidade de status), não de qualquer número com quem você troca DM. Ver Status / Stories.
Campo quoted_id (reply)#
Todos os endpoints de envio que listam quoted_id aceitam:
- UUID Catcher (formato
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx): e o mesmo UUID retornado emmessage_idsnos webhooks (message.received,message.sent, etc). A API resolve internamente para o ID hexadecimal do WhatsApp antes de enfileirar. - ID hexadecimal do WhatsApp (ex.:
3EB0ABC123...): passa direto, sem lookup.
Se o quoted_id for um UUID e não existir no banco (mensagem removida, pertence a outra instância, ou nunca foi recebida por está instância), a API responde 404 Not Found com error_code=MESSAGE_NOT_FOUND. Não ha enfileiramento parcial: a mensagem so e enviada com reply válido ou não e enviada.
Isto vale para
SendText,SendImage,SendVideo,SendAudio,SendDocument,SendSticker,SendLocation,SendContacte para os equivalentes de envio agendado viaPOST /v1/instances/{instanceId}/messages/scheduled.
Modo passivo (409 PASSIVE_MODE_ENABLED)#
Quando a instância está configurada em modo passivo (passive_mode_enabled=true em GET/PATCH /v1/instances/{id}/settings), todos os endpoints de envio (text, image, video, audio, document, sticker, location, contact, reaction, poll, template, forward, delete, edit) são bloqueados antes do enfileiramento:
{
"error_code": "PASSIVE_MODE_ENABLED",
"message": "instance is in passive mode — set passive_mode_enabled=false in /v1/instances/{id}/settings or pass X-Force-Send: true to override (audit-logged)",
"trace_id": "..."
}
- HTTP
409 Conflict. - Bypass: header
X-Force-Send: true(registra audit logaudit_action=passive_mode.force_send). - Modo passivo falha fechado: um erro transitorio no banco do tenant retorna
503/500em vez de liberar o envio (proposito: não vazar outbound de uma instância sob hold legal/monitoramento).
Guard anti-spam (409 BLOCKED_*)#
Endpoints de envio passam por um guard anti-spam antes de enfileirar. Qualquer regra que dispare devolve 409 Conflict:
error_code |
Regra | Campos extras no payload |
|---|---|---|
BLOCKED_DUPLICATE_CONTENT |
Conteúdo identico já enviado nas últimas 24h | remote_jid, content_hash, first_sent_at |
BLOCKED_NO_RECIPROCITY |
Envios consecutivos sem nenhum inbound do contato — limite fixo | remote_jid, count, threshold |
BLOCKED_TEMPERATURE_LIMIT |
max_consecutive da curva de temperatura do contato excedido |
remote_jid, score, tier, max_consecutive, outbound_consecutive |
- A regra de conteúdo duplicado é controlada por
anti_spam_guard_enabled(defaulttrue). - O limite consecutivo sem resposta é controlado pela combinação
anti_spam_guard_enabled+anti_spam_temperature_enabled. Quando ambas estão ativas (default), a curva de temperatura por contato substitui o limite fixo — apenasBLOCKED_TEMPERATURE_LIMITpode disparar. Quando a temperatura está desativada, apenasBLOCKED_NO_RECIPROCITYpode disparar. As duas nunca rodam ao mesmo tempo. delete,edit,reactioneforwardsão isentos da regra de conteúdo duplicado (não ha shape de conteúdo comparavel), mas continuam contando para o limite consecutivo.- Bypass: header
X-Force-Send: true(registra audit logaudit_action=anti_spam.force_send).
POST/v1/instances/{instanceId}/messages/text#
Envia mensagem de texto.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"text": "Ola! Como posso ajudar?",
"quoted_id": "3EB0ABC123...",
"mentions": ["5541988888888@s.whatsapp.net"]
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário (telefone ou JID) |
text |
string | sim | Conteúdo da mensagem |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs de usuários mencionados |
Regras do humanize:
- Controlado por instância via
GET/PATCH /v1/instances/{instanceId}/settings(humanize_enabled, defaulttrue). - Quando ativo, o processor envia
composingimediatamente antes deSendTexte esperanumero_de_caracteres(texto) * multipliersegundos, commultiplieraleatorio entre0.1e0.5. - A aplicação ocorre apenas em mensagens de texto para chats 1:1; grupos (
@g.us) e mensagens de mídia seguem o fluxo legado. - Se a leitura da configuração falhar, o envio segue imediatamente para não bloquear a fila.
Envio de mídia: media_url, media_id ou arquivo direto#
Os endpoints POST /v1/instances/{instanceId}/messages/{image|video|audio|document|sticker} aceitam as tres formas de origem no mesmo endpoint:
application/jsoncommedia_url: o servidor baixa a URL sincronamente durante o request (~500ms-2s para arquivos pequenos), aplica validação SSRF/MIME/64 MB, armazena no R2/S3, gera ummedia_idinterno e enfileira o envio usando esse ID.application/jsoncommedia_id: reutiliza uma mídia já armazenada pelo endpointPOST /v1/instances/{instanceId}/mediaou por mídia recebida.multipart/form-datacom campofile: envia o arquivo binario direto no próprio endpoint; a API armazena em R2, gera ummedia_ide enfileira o envio usando esse ID.
Arquitetura de mídia: API resolve, worker só envia#
Independente da forma de origem (URL, media_id existente ou multipart), a API sempre garante que os bytes estejam armazenados no R2 antes de retornar 202. O worker só recebe media_id na fila Asynq — nunca uma URL externa para baixar. Isso elimina classe inteira de falha silenciosa em que a CDN externa bloqueia download depois do 202 e a mensagem nunca chega.
Prático para você (consumidor da API):
- Falha de download é sincrona: se a URL retorna 4xx, 5xx, timeout, ou MIME bloqueado, você recebe
502 MEDIA_FETCH_FAILEDno momento da chamada. NÃO vai pra fila, não tem retry silencioso. Retry o request com URL válida ou faca upload prévio. - Custo de latência: requests com
media_urlagora levam ~500ms-2s a mais que commedia_id(tempo do download + upload R2). Para arquivos grandes (>10MB), considere fazer upload prévio viaPOST /v1/instances/{id}/mediae reutilizar omedia_idretornado. - Cache automático: cada
media_urlbaixado vira uma linha no R2 comsource="url-cache"e TTL de 7 dias. Reaproveitamento da mesma URL durante esse período é livre. - Disciplina de proxy: o download da URL passa pelo cliente HTTP padrão da API (resolver DNS-over-TLS, validador SSRF). Bright Data residential proxy é reservado APENAS para o upload
worker → Meta CDNna hora do envio. Isso é deliberado e estrutural — ver.claude/rules/whatsapp-proxy-discipline.md.
Campos textuais do envio (to, caption, file_name, ptt, quoted_id, mentions) podem ir no multipart como campos de formulario. mentions aceita valores repetidos, CSV ou JSON array.
O header
Idempotency-Keye obrigatório em todas as tres formas de envio de mídia (igual aos demais endpoints de mensagem).
Exemplo com arquivo direto:
curl -X POST "https://api.catcher.one/v1/instances/$INSTANCE/messages/image" \
-H "X-API-Key: $TOKEN" \
-H "Idempotency-Key: req_01HX9Y..." \
-F "to=554137984905" \
-F "caption=Imagem enviada pela API" \
-F "file=@/caminho/foto.png"
Exemplo com URL externa:
curl -X POST "https://api.catcher.one/v1/instances/$INSTANCE/messages/video" \
-H "X-API-Key: $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: req_01HX9Y..." \
-d '{
"to": "554137984905",
"media_url": "https://meucdn.com/video.mp4",
"caption": "Olha esse vídeo"
}'
# → 202 só sai depois que os bytes estão no R2.
# → 502 MEDIA_FETCH_FAILED se o CDN externo recusar.
Exemplo com upload prévio (recomendado para arquivos grandes ou reutilizados):
# 1. Upload uma vez:
MEDIA_ID=$(curl -sf -X POST "https://api.catcher.one/v1/instances/$INSTANCE/media" \
-H "X-API-Key: $TOKEN" \
-H "Idempotency-Key: upload-$(date +%s)" \
-F "file=@/caminho/video-grande.mp4" | jq -r .media_id)
# 2. Reutilize em N envios sem rebaixar:
for PHONE in 554137984905 554196332719 554163475715; do
curl -X POST "https://api.catcher.one/v1/instances/$INSTANCE/messages/video" \
-H "X-API-Key: $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: bulk-$PHONE-$(date +%s)" \
-d "{\"to\":\"$PHONE\",\"media_id\":\"$MEDIA_ID\",\"caption\":\"Veja como ficou\"}"
done
Em JSON, envie
media_urlOUmedia_id. Em multipart, enviefile. Um desses tres e obrigatório.
POST/v1/instances/{instanceId}/messages/image#
Envia imagem.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"media_url": "https://exemplo.com/foto.png",
"caption": "Confira esta imagem!",
"quoted_id": "",
"mentions": []
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
media_url |
string | sim* | URL pública da imagem |
media_id |
string | sim* | ID de média pre-enviado via upload |
file |
file | sim* | Arquivo direto via multipart/form-data |
caption |
string | não | Legenda da imagem |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs mencionados |
*Enviar
media_url,media_idOUfile(um dos tres e obrigatório).
POST/v1/instances/{instanceId}/messages/video#
Envia video.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"media_url": "https://exemplo.com/video.mp4",
"caption": "Assista ao video"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
media_url |
string | sim* | URL pública do video |
media_id |
string | sim* | ID de média pre-enviado |
file |
file | sim* | Arquivo direto via multipart/form-data |
caption |
string | não | Legenda |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs mencionados |
*Enviar
media_url,media_idOUfile(um dos tres e obrigatório).
POST/v1/instances/{instanceId}/messages/audio#
Envia audio. Por padrão, todos os audios são enviados como nota de voz (PTT). Formatos não-OGG (MP3, M4A, WAV, WebM, etc) são convertidos automaticamente para OGG Opus (mono, 16 kHz, 32 kbps) via ffmpeg — formato exigido pelo WhatsApp mobile para PTT. Uma waveform de 64 bytes e gerada automaticamente para exibição no app. Use ptt: false para enviar como audio regular sem conversao.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"media_url": "https://exemplo.com/audio.mp3"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
media_url |
string | sim* | URL pública do audio (qualquer formato: MP3, OGG, M4A, WAV, etc) |
media_id |
string | sim* | ID de média pre-enviado |
file |
file | sim* | Arquivo direto via multipart/form-data |
ptt |
bool | não | Padrão true (nota de voz). Converte automaticamente para OGG Opus se necessário. false = audio regular, sem conversao |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs mencionados |
*Enviar
media_url,media_idOUfile(um dos tres e obrigatório).
No envio multipart, o campo
pttdeve ser um booleano válido (true/false); valores inválidos retornam400 BAD_REQUEST.
POST/v1/instances/{instanceId}/messages/document#
Envia documento (PDF, DOCX, etc).
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"media_url": "https://exemplo.com/relatorio.pdf",
"file_name": "relatorio-marco.pdf"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
media_url |
string | sim* | URL pública do documento |
media_id |
string | sim* | ID de média pre-enviado |
file |
file | sim* | Arquivo direto via multipart/form-data |
file_name |
string | não | Nome do arquivo exibido no WhatsApp |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs mencionados |
*Enviar
media_url,media_idOUfile(um dos tres e obrigatório).
POST/v1/instances/{instanceId}/messages/sticker#
Envia sticker (figurinha). A imagem deve estar no formato WebP.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"media_url": "https://exemplo.com/sticker.webp"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
media_url |
string | sim* | URL pública do sticker (WebP) |
media_id |
string | sim* | ID de média pre-enviado |
file |
file | sim* | Arquivo direto via multipart/form-data |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs para mencionar |
*Enviar
media_url,media_idOUfile(um dos tres e obrigatório).
POST/v1/instances/{instanceId}/messages/location#
Envia localização.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"latitude": -25.4284,
"longitude": -49.2733,
"name": "Praca Tiradentes",
"address": "Centro, Curitiba - PR"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
latitude |
float | sim | Latitude |
longitude |
float | sim | Longitude |
name |
string | não | Nome do local |
address |
string | não | Endereço |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs para mencionar |
POST/v1/instances/{instanceId}/messages/contact#
Envia um ou mais cartoes de contato (vCard). Com 2+ contatos, o WhatsApp do
destinatário recebe um único card agrupado (ContactsArrayMessage).
Auth: Todos autenticados
Request — contato único (nome + telefone):
{
"to": "554137984905",
"contact_name": "Suporte Catcher",
"contact_phone": "+5541988887777"
}
Request — varios contatos:
{
"to": "554137984905",
"contacts": [
{ "contact_name": "Suporte Catcher", "contact_phone": "+5541988887777" },
{ "contact_name": "Comercial", "contact_phone": "+5541977776666" }
]
}
Request — vCard cru (fidelidade total: email, empresa, multiplos números):
{
"to": "554137984905",
"vcard": "BEGIN:VCARD\nVERSION:3.0\nFN:Suporte Catcher\nTEL;type=CELL;waid=5541988887777:+5541988887777\nEMAIL:suporte@catcher.one\nEND:VCARD"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
contact_name |
string | condicional | Nome do contato. Obrigatório no modo contato-único quando vcard e contacts estão ausentes |
contact_phone |
string | condicional | Telefone do contato em formato E.164 com + (ex: +5541988887777). Obrigatório junto de contact_name |
vcard |
string | não | vCard 3.0 cru de um único contato (deve conter BEGIN:VCARD/END:VCARD, max 8 KB). Enviado verbatim — o conteúdo e responsabilidade do chamador. Tem precedencia sobre contact_name/contact_phone |
contacts |
[]object | não | Lista de contatos (max 50). Cada item: contact_name+contact_phone, ou vcard. Tem precedencia sobre os campos de nivel raiz. 2+ itens viram um ContactsArrayMessage |
quoted_id |
string | não | ID da mensagem respondida (UUID Catcher ou hex WhatsApp; ver nota geral de quoted_id acima) |
mentions |
[]string | não | JIDs para mencionar |
Precedencia entre as formas:
contacts>vcard>contact_name/contact_phone. O webhookmessage.sentresultante carrega o arraycontacts(ver message.sent).
Erros de validação (400):
| Mensagem | Causa |
|---|---|
too many contacts: at most 50 per send |
contacts[] excedeu o cap de 50 itens |
vcard must be a vCard 3.0 string (BEGIN/END:VCARD) under 8 KB |
vcard no nivel raiz não tem BEGIN:VCARD/END:VCARD ou passou de 8 KB |
contacts[N]: vcard must be a vCard 3.0 string (BEGIN/END:VCARD) under 8 KB |
Item N do array contacts com vcard inválido ou oversized |
valid contact_name and contact_phone (or a vcard) are required |
Modo legacy: faltou contact_name, ou contact_phone não está em E.164 com + |
contacts[N]: valid contact_name and contact_phone (or a vcard) are required |
Item N do array sem contact_name+contact_phone válidos nem vcard |
POST/v1/instances/{instanceId}/messages/reaction#
Envia reacao (emoji) a uma mensagem.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"message_id": "198d2663-d6a7-4d62-a018-963065c80403",
"emoji": "👍"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | JID do chat onde está a mensagem |
message_id |
string | sim | UUID Catcher (elemento de message_ids nos webhooks) OU hex WhatsApp da mensagem-alvo. A API resolve internamente; retorna 404 MESSAGE_NOT_FOUND se o UUID não existir nesta instância. |
emoji |
string | sim | Emoji da reacao (string vazia para remover) |
POST/v1/instances/{instanceId}/messages/poll#
Cria uma enquete.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"question": "Qual o melhor dia para a reuniao?",
"options": ["Segunda", "Terca", "Quarta"],
"max_selections": 1
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
question |
string | sim | Pergunta da enquete |
options |
[]string | sim | Opcoes (mínimo 2) |
max_selections |
int | não | Máximo de selecoes por pessoa. Padrão: 1 |
POST/v1/instances/{instanceId}/messages/template#
Envia mensagem com botoes (template). Requer conta WhatsApp Business.
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"body_text": "Escolha uma opcao abaixo:",
"footer_text": "Powered by Catcher",
"buttons": [
{"text": "Comprar", "id": "btn_buy"},
{"text": "Cancelar", "id": "btn_cancel"}
]
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário |
body_text |
string | sim | Texto principal |
footer_text |
string | não | Texto do rodape |
buttons |
[]object | sim | 1 a 3 botoes |
buttons[].text |
string | sim | Texto exibido no botao |
buttons[].id |
string | sim | ID do botao (retornado no callback) |
Nota: Se a instância não for uma conta Business, a mensagem será rejeitada sem retry.
POST/v1/instances/{instanceId}/messages/link-preview#
Envia uma mensagem de texto com card de preview de link controlado (você define título e descrição). Renderiza no destinatário (tipo nativo ExtendedTextMessage).
Auth: Todos autenticados · Header: Idempotency-Key obrigatório
Preview NÃO é o default. Um link num
POST /messages/textcomum vai como texto puro — o app do destinatário PODE gerar um preview básico do lado dele (fetch client-side), mas isso não é garantido nem controlado por você. Para um card com título/descrição garantidos, use este endpoint.
Request:
{
"to": "554137984905",
"text": "Conhece a Catcher? https://catcher.one",
"url": "https://catcher.one",
"title": "Catcher",
"description": "WhatsApp API multi-tenant"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to |
string | sim | Destinatário |
text |
string | sim | Corpo da mensagem (deve conter a URL) |
url |
string | sim | URL do preview |
title |
string | não | Título do card |
description |
string | não | Descrição do card |
quoted_id |
string | não | UUID Catcher OU hex WhatsApp da mensagem citada |
Retorna 202 com message_id (UUID Catcher) + task_id + idempotency_key. Emite message.sent (message_type: text).
POST/v1/instances/{instanceId}/messages/event#
Envia uma mensagem de evento (calendário). Tipo nativo EventMessage — renderiza no destinatário.
Auth: Todos autenticados · Header: Idempotency-Key obrigatório
Request:
{
"to": "554137984905",
"name": "Reunião de planejamento",
"description": "pauta trimestral",
"start_at": "2026-05-30T15:00:00Z",
"end_at": "2026-05-30T16:00:00Z",
"location": "Online"
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to |
string | sim | Destinatário (1:1 ou grupo @g.us) |
name |
string | sim | Nome do evento |
start_at |
string | sim | Início (RFC3339) |
description |
string | não | Descrição |
end_at |
string | não | Fim (RFC3339) |
location |
string | não | Local |
link |
string | não | Link de chamada/join |
Retorna 202. Emite message.sent (message_type: event).
POST/v1/instances/{instanceId}/messages/ptv#
Envia um vídeo redondo (PTV / "video note"). Mesma origem de mídia dos outros sends (media_url / media_id / multipart). Tipo nativo PtvMessage.
POST/v1/instances/{instanceId}/messages/gif#
Envia um vídeo animado marcado como GIF (autoplay/loop no destinatário, gifPlayback=true). Mesma origem de mídia.
Ambos: 202 + Idempotency-Key obrigatório; emitem message.sent (message_type: ptv / gif).
view_once (flag de mídia)#
POST /messages/image, /video e /audio aceitam "view_once": true no corpo — a mídia vira visualização única (viewOnceMessage, auto-destrói após aberta). Default false.
{ "to": "554137984905", "media_url": "https://...", "view_once": true }
Status / Stories (§13.3)#
Posta um status (story). Nativo e render-safe: a whatsmeow envia para
status@broadcast e resolve a audiência automaticamente das configurações
de privacidade de status do número (quem vê), cifrando o fan-out. Sem to —
status é um broadcast. Anti-spam guard não se aplica (broadcast, não 1:1);
o throttle de pacing continua valendo.
Auth: Todos autenticados · Header: Idempotency-Key obrigatório nos POSTs.
POST /v1/instances/{instanceId}/messages/status/text#
{ "text": "Meu status ✨", "background_color": "#1FA75B", "font": 6 }
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
text |
string | sim | Texto do status |
background_color |
string | não | Cor de fundo #RRGGBB |
font |
int | não | Fonte (0=sistema, 1=system_text, 2=fb_script, 6=system_bold, 7=morningbreeze, 8=calistoga, 10=courierprime_bold) |
POST /v1/instances/{instanceId}/messages/status/image#
POST /v1/instances/{instanceId}/messages/status/video#
{ "media_url": "https://...", "caption": "legenda opcional" }
Aceita media_url (resolvido sincronamente pra media_id) OU media_id (upload prévio em /media).
Todos retornam 202 com message_id (UUID Catcher); emitem message.sent
(message_type: status_text / status_image / status_video).
GET /v1/instances/{instanceId}/status/privacy#
Auth: Owner/Admin
Retorna quem pode ver o status do número.
{ "status_privacy": [ { "type": "contacts", "list": [], "is_default": true } ] }
type: contacts | blacklist | whitelist. list: telefones (para black/whitelist).
GET /v1/instances/{instanceId}/status/{statusId}/views#
Auth: Owner/Admin
Contagem de visualizações + lista de quem viu UM status seu. statusId é o
message_id retornado ao postar (UUID Catcher) ou o id bruto do WhatsApp.
A contagem é de viewers únicos (um por contato, mesmo que reabra o status).
{
"status_message_id": "c710cea9-fcc4-4a7c-9e30-1a19ebc07bfb",
"count": 2,
"viewers": [
{ "phone": "554196332719", "jid": "216251804708983@lid", "receipt_type": "read", "viewed_at": "2026-05-23T18:48:26Z" },
{ "phone": "554163475715", "jid": "...@lid", "receipt_type": "played", "viewed_at": "2026-05-23T18:49:01Z" }
]
}
receipt_type: read (status aberto) | played (vídeo assistido). Sem visualizações ainda → count: 0, viewers: [].
Webhooks de status — recepção (interações com seu status)#
A Catcher emite um evento dedicado para cada interação. Compartilhamento não é observável pelo autor (o WhatsApp não notifica o autor quando alguém compartilha).
Semântica de público (audience) — vale para TODOS os eventos de recepção abaixo. Status são entregues ao público do autor: contatos salvos na agenda do número, filtrados pela privacidade de status ("Meus contatos" / exceções). Você só recebe
status.receivedde um contato cujo status alcança a sua instância, e só recebestatus.viewed/status.reaction/status.replyde quem está no SEU público. Trocar DM com um número não o coloca no público de status de nenhum dos lados — é preciso estar salvo na agenda. Em testes entre instâncias de API que não se têm salvas, oPOST .../status/*envia com sucesso (você vêmessage.sentno autor), mas a outra instância não emitestatus.received— isso é esperado, não é bug. Um avisoServer returned different participant list hash …no log do worker é a computação do público, não uma falha de envio.
status.received — um contato postou um status/story (antes era descartado):
{ "phone": "5511999999999", "from": "5511999999999@s.whatsapp.net",
"status_message_id": "3EB0...", "type": "text|image|video|audio",
"content": "...", "timestamp": 1779543210 }
Convenção de campos (igual a todos os eventos):
phone= telefone do interagente (resolvido),from= JID dele (pode ser um LID em status@broadcast).
status.viewed — um contato VISUALIZOU um status SEU (recibo read/played):
{ "phone": "554196332719", "from": "216251804708983@lid",
"status_message_id": "c710cea9-...", "whatsapp_status_id": "3EB0...",
"status_type": "image", "receipt_type": "read", "timestamp": 1779565914 }
status.reaction — um contato REAGIU a um status SEU:
{ "phone": "554196332719", "from": "216251804708983@lid", "push_name": "Adriano",
"status_message_id": "c710cea9-...", "whatsapp_status_id": "3EB0...",
"status_type": "image", "emoji": "💚", "removed": false, "timestamp": 1779565792 }
removed: true (e emoji: "") quando a reação foi removida.
status.reply — um contato RESPONDEU/comentou um status SEU. A message.received
do DM continua disparando normalmente; este evento traz o contexto do status:
{ "phone": "554196332719", "from": "216251804708983@lid", "push_name": "Adriano",
"status_message_id": "c710cea9-...", "whatsapp_status_id": "3EB0...",
"status_type": "image", "reply_message_id": "0a279...", "whatsapp_reply_id": "3A2D...",
"reply_type": "text", "content": "😍", "timestamp": 1779566005 }
Mensagens interativas (botões / PIX / OTP / carrossel / lista) — DESATIVADAS#
Os endpoints POST /messages/button, /button-pix, /button-otp e /option-list existem no código mas estão desativados e retornam 403 INTERACTIVE_DISABLED.
Por quê: o spike de 2026-05-23 provou que os clientes WhatsApp do destinatário descartam silenciosamente mensagens interativas (InteractiveMessage/ListMessage) enviadas pelo protocolo multi-device não-oficial — não renderizam, nem como texto. O Baileys (referência da indústria) nunca shipou envio de botão pelo mesmo motivo. Enviar tipos não suportados é risco de ban sem ganho. Ficam prontos para reativar se a Meta passar a suportar. Detalhe em docs/COMPARATIVO-ZAPI-BIAZAP.md §15.9.
POST /v1/instances/{instanceId}/messages/button#
Retorna 403 INTERACTIVE_DISABLED enquanto mensagens interativas seguem desativadas.
POST /v1/instances/{instanceId}/messages/button-pix#
Retorna 403 INTERACTIVE_DISABLED enquanto mensagens interativas seguem desativadas.
POST /v1/instances/{instanceId}/messages/button-otp#
Retorna 403 INTERACTIVE_DISABLED enquanto mensagens interativas seguem desativadas.
POST /v1/instances/{instanceId}/messages/option-list#
Retorna 403 INTERACTIVE_DISABLED enquanto mensagens interativas seguem desativadas.
POST/v1/instances/{instanceId}/messages/forward#
Encaminha uma mensagem existente para outro chat.
Auth: Todos autenticados
Request:
{
"to": "5541988888888",
"forward_chat_id": "554137984905@s.whatsapp.net",
"message_id": "198d2663-d6a7-4d62-a018-963065c80403"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | Destinatário do encaminhamento |
forward_chat_id |
string | sim | JID do chat onde a mensagem original está |
message_id |
string | sim | UUID Catcher (elemento de message_ids nos webhooks) OU hex WhatsApp da mensagem a encaminhar. A API resolve internamente; retorna 404 MESSAGE_NOT_FOUND se o UUID não existir nesta instância. |
Funciona com todos os tipos: texto, imagem, video, audio, documento.
DELETE/v1/instances/{instanceId}/messages/{messageId}#
Apaga uma mensagem enviada ("apagar para todos").
Auth: Todos autenticados
Request:
{
"to": "554137984905",
"message_id": "198d2663-d6a7-4d62-a018-963065c80403"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | JID do chat |
message_id |
string | sim | UUID Catcher (elemento de message_ids nos webhooks) OU hex WhatsApp da mensagem a apagar. A API resolve internamente; retorna 404 MESSAGE_NOT_FOUND se o UUID não existir nesta instância. |
Resposta 202:
{
"status": "queued",
"task_id": "asynq:task:xxxx"
}
PUT/v1/instances/{instanceId}/messages/{messageId}/edit#
Edita o texto de uma mensagem enviada. Somente mensagens de texto outbound podem ser editadas. O próprio WhatsApp pode rejeitar edicoes de mensagens muito antigas (tipicamente após ~15 minutos) — a API não impoe esse limite, a rejeição ocorre de forma assincrona no worker.
Auth: Todos autenticados
Request:
{
"content": "Texto atualizado da mensagem"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
content |
string | sim | Novo conteúdo de texto da mensagem |
Resposta 202:
{
"status": "queued",
"task_id": "asynq:task:xxxx"
}
Erros:
| Código | Descricao |
|---|---|
| 400 | content is required — conteúdo vazio |
| 400 | only outbound messages can be edited — mensagem não foi enviada pela instância |
| 400 | only text messages can be edited — tipo não e texto |
| 400 | cannot edit a deleted message — mensagem já foi deletada |
| 404 | message not found — messageId não encontrado (UUID inexistente nesta instância OU hex desconhecido) |
O
messageIdna URL aceita o UUID Catcher (elemento demessage_idsnos webhooks) OU o hex WhatsApp (whats_app_id). A API resolve internamente; UUID inexistente retorna404 MESSAGE_NOT_FOUND. Após a edicao, o webhookmessage.editede emitido comis_from_me: true.
7.X Ações sobre mensagens (Wave 1 — Z-API parity)#
Três endpoints síncronos que operam sobre uma mensagem existente: fixar (pin), votar em enquete (poll-vote) e favoritar (star). Diferente dos sends de conteúdo, esses endpoints respondem 200 OK (não 202) porque atuam direto via RPC no worker — sem fila Asynq, sem Idempotency-Key obrigatório, sem anti-spam guard pesado.
Os 3 endpoints aceitam o message_id em qualquer formato (UUID Catcher OU hex WhatsApp). A API resolve internamente.
POST /v1/instances/{instanceId}/messages/pin#
Fixa ou desfixa uma mensagem no chat para todos os participantes. Em grupos, requer que a instância seja admin.
Request:
{
"chat_jid": "554163475715@s.whatsapp.net",
"message_id": "198d2663-d6a7-4d62-a018-963065c80403",
"target_sender_jid": "",
"target_from_me": true,
"pin": true,
"duration_seconds": 86400
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
chat_jid |
string | sim | Chat onde a mensagem está. |
message_id |
string | sim | UUID Catcher OU hex WhatsApp da mensagem-alvo. |
target_sender_jid |
string | não | JID de quem enviou a mensagem-alvo. Em DM, default = chat_jid. Obrigatório em grupos. |
target_from_me |
bool | não | Se a mensagem foi enviada pela instância. Default false. |
pin |
bool | sim | true = PIN_FOR_ALL, false = UNPIN_FOR_ALL. |
duration_seconds |
int | não | 86400 (24h) / 604800 (7d) / 2592000 (30d). 0 = WhatsApp default (24h). Ignorado em unpin. |
Response 200 OK:
{
"instance_id": "48a1322e-f3b1-4a32-ac79-58f67036af79",
"chat_jid": "554163475715@s.whatsapp.net",
"target_message_id": "3EB01870039BE5362C6F0A",
"action": "pinned",
"envelope_message_id": "3EB0ABC123..."
}
envelope_message_id é o ID do WhatsApp da própria mensagem PIN (não da mensagem fixada).
POST /v1/instances/{instanceId}/messages/poll-vote#
Vota em uma enquete existente. A instância precisa ter recebido a enquete (whatsmeow precisa do segredo da poll pra re-encriptar).
Request:
{
"chat_jid": "554163475715@s.whatsapp.net",
"poll_message_id": "3EB0CAFEBABE123",
"poll_sender_jid": "",
"poll_from_me": false,
"selected_options": ["sim", "muito legal"]
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
chat_jid |
string | sim | Chat onde a poll está. |
poll_message_id |
string | sim | UUID Catcher OU hex WhatsApp da mensagem de criação da poll. |
poll_sender_jid |
string | não | Quem criou a poll. Em DM, default = chat_jid. |
poll_from_me |
bool | não | Se a instância criou a poll. Default false. |
selected_options |
array de strings | sim | Nomes exatos das opções votadas. [] vazio limpa o voto. |
Response 200 OK:
{
"instance_id": "...",
"chat_jid": "554163475715@s.whatsapp.net",
"poll_message_id": "3EB0CAFEBABE123",
"selected_options": ["sim", "muito legal"],
"envelope_message_id": "3EB0VOTE..."
}
Voto é uma
PollUpdateMessagecifrada com o segredo da poll original. Se a instância não tem o segredo (poll foi enviada antes da instância existir, ou após reset de sessão), o envio falha com404.
POST /v1/instances/{instanceId}/messages/star#
Favorita ou desfavorita uma mensagem. Muda só no AppState da instância — propaga pros outros devices via sync (não é visível pro destinatário).
Request:
{
"chat_jid": "554163475715@s.whatsapp.net",
"message_id": "198d2663-d6a7-4d62-a018-963065c80403",
"sender_jid": "",
"from_me": true,
"starred": true
}
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
chat_jid |
string | sim | Chat onde a mensagem está. |
message_id |
string | sim | UUID Catcher OU hex WhatsApp. |
sender_jid |
string | não | Quem enviou. Em DM, default = chat_jid. Obrigatório em grupos. |
from_me |
bool | não | Se a instância enviou. Default false. |
starred |
bool | sim | true = favoritar, false = desfavoritar. |
Response 200 OK:
{
"instance_id": "...",
"chat_jid": "554163475715@s.whatsapp.net",
"message_id": "3EB01870...0A",
"action": "starred"
}
Mudança propaga pros outros devices da operadora via
appstate sync. Webhookmessage.starredé emitido quando a propagação retorna pelo whatsmeow.