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.
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 de 3 regras 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 | remote_jid, count, threshold |
BLOCKED_TEMPERATURE_LIMIT |
max_consecutive do tier de temperatura do contato excedido |
remote_jid, score, tier, max_consecutive, outbound_consecutive |
- As regras 1 e 2 são controladas por
anti_spam_guard_enabled(defaulttrue); a regra 3 poranti_spam_temperature_enabled+ flag global de plataforma. delete,edit,reactioneforwardsão isentos da regra de conteúdo duplicado (não ha shape de conteúdo comparavel), mas continuam contando reciprocidade.- 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": "3EB0ABC123DEF456",
"emoji": "👍"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | JID do chat onde está a mensagem |
message_id |
string | sim | ID da mensagem no WhatsApp |
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/forward#
Encaminha uma mensagem existente para outro chat.
Auth: Todos autenticados
Request:
{
"to": "5541988888888",
"forward_chat_id": "554137984905@s.whatsapp.net",
"message_id": "3EB0ABC123DEF456"
}
| 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 | ID da mensagem a encaminhar |
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": "3EB0ABC123DEF456"
}
| Campo | Tipo | Obrigatório | Descricao |
|---|---|---|---|
to |
string | sim | JID do chat |
message_id |
string | sim | ID da mensagem no WhatsApp |
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 |
O
messageIdna URL e o ID do WhatsApp (whats_app_id), não o ID interno. Após a edicao, o webhookmessage.editede emitido comis_from_me: true.