Tema
Enviar mensagens
Três endpoints de envio, todos assíncronos: a resposta 202 Accepted confirma que a mensagem entrou na fila, não que ela chegou ao cliente final. Para saber o que aconteceu de fato, consulte a consulta de status.
| Endpoint | Envia |
|---|---|
POST /v1/messages/text | Texto livre |
POST /v1/messages/media | Mídia (imagem, vídeo, áudio ou documento), com legenda opcional |
POST /v1/messages/template | Um template aprovado, dentro ou fora da janela de 24 horas |
Escopo exigido nos três: messages:send.
Cabeçalho obrigatório: Idempotency-Key
Todo POST de envio exige o cabeçalho Idempotency-Key. Sem ele, o pedido é recusado com validation_failed.
Um sistema que integra por HTTP eventualmente reenvia uma chamada: timeout de rede, uma fila que reprocessa, um retry automático do seu lado. Sem uma chave de idempotência, esse reenvio vira uma segunda mensagem de verdade, por exemplo o mesmo boleto duas vezes no WhatsApp do cliente final.
Como funciona:
- Escolha um valor único por pedido de negócio, por exemplo o id da cobrança no seu sistema, não um valor sorteado a cada tentativa.
- Reenviar com a mesma chave e o mesmo corpo devolve a resposta da primeira chamada, sem criar uma segunda mensagem.
- Reenviar com a mesma chave e um corpo diferente é recusado com o código
idempotency_key_reused(409): o Konversia nunca aplica o corpo novo por cima do antigo. - A chave fica válida por 24 horas. Depois disso, o mesmo valor é tratado como um pedido novo.
Guarda de mensagem automática duplicada
Além da Idempotency-Key, existe um segundo guarda, automático e sem qualquer configuração do lado do integrador: o Konversia descarta a segunda mensagem automática com o mesmo conteúdo, para a mesma conversa, dentro de uma janela de 90 segundos.
Isso não é o mesmo mecanismo da Idempotency-Key. A chave protege contra o reenvio da mesma chamada (mesma chave, mesmo corpo); este guarda protege contra duas chamadas diferentes, com chaves de idempotência diferentes, que por coincidência carregam o mesmo texto para a mesma conversa. Caso real: um ERP mandando "Recebemos seu pagamento" para duas faturas diferentes do mesmo cliente, com o texto idêntico nas duas chamadas.
O comportamento nesse caso é silencioso: as duas chamadas recebem o mesmo 202 Accepted de sempre, sem nenhum erro. A segunda mensagem, porém, nunca chega a sair: ela fica com status: "skipped" e a razão do bloqueio em reason, visível só em GET /v1/messages/{id}.
Para não cair nisso quando o conteúdo pode legitimamente se repetir, inclua algo que varie de uma chamada para a outra, por exemplo o número da fatura: "Recebemos seu pagamento da fatura 8842." em vez de "Recebemos seu pagamento."
Para onde a mensagem vai
Os três endpoints resolvem o destino da mesma forma, a partir destes campos:
to(obrigatório): telefone do destinatário, com ou sem+e DDI.channel_id(opcional): qual linha de WhatsApp usar. Sem ele, a API usa o canal padrão da chave, se ela tiver um configurado, ou o único canal utilizável da empresa; havendo mais de um canal e nenhum padrão, o pedido é recusado comchannel_not_founde a lista de canais disponíveis, para você escolher um.contact_name(opcional): nome do contato, usado só quando ele ainda não existe na empresa.conversation_status(opcional): status inicial, só quando a conversa é criada agora (not_started,pending,in_progressouresolved; padrãopending).
Uma conversa já existente nunca tem o setor ou o atendente responsável alterado por um envio da API: a mensagem só entra nela, como qualquer outra mensagem entraria.
O status é a única exceção, e só num caso específico: se a conversa ainda estava not_started (aberta manualmente, sem nenhuma interação até então), o envio a promove para in_progress, porque uma mensagem realmente saindo é o sinal de que o atendimento começou. Essa é a mesma regra que já vale para CRM, workflow e para a tela; uma conversa em qualquer outro status (pending, in_progress, resolved) nunca muda de status por causa de um envio da API.
A janela de 24 horas
Regra da Meta para o canal oficial: depois de 24 horas sem mensagem do cliente final, só um template aprovado reabre a conversa. Texto ou mídia livre fora dessa janela é recusado.
Em POST /v1/messages/text e POST /v1/messages/media, informe fallback_template para não precisar checar a janela antes de cada envio:
json
{
"to": "5511999998888",
"text": "Seu boleto está disponível.",
"fallback_template": {
"name": "aviso_boleto",
"language": "pt_BR",
"variables": ["1234"]
}
}- Dentro da janela:
fallback_templateé ignorado, e o texto (ou a mídia) sai normalmente. - Fora da janela: o Konversia manda o template no lugar do conteúdo original.
- Fora da janela e sem
fallback_template: o pedido é recusado comoutside_24h_window(422), e a resposta já traz emerror.details.approved_templatesa lista de templates aprovados que servem de fallback, para você não precisar de uma segunda chamada.
Mídia é diferente. Fora da janela, o fallback só existe em texto, então o anexo original seria descartado em silêncio. Por isso POST /v1/messages/media fora da janela sempre recusa com media_requires_active_window (422), mesmo com fallback_template informado. Envie dentro da janela, ou use POST /v1/messages/text.
Para enviar um template por escolha própria, dentro ou fora da janela, use POST /v1/messages/template diretamente: esse endpoint nunca checa a janela de 24 horas, porque ali template já é a decisão explícita de quem integra.
Enviar texto
POST /v1/messages/text
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Telefone do destinatário |
text | string | sim | Até 4096 caracteres |
channel_id | integer | não | Ver Para onde a mensagem vai |
contact_name | string | não | Até 255 caracteres |
conversation_status | string | não | Ver Para onde a mensagem vai |
fallback_template | objeto | não | Ver A janela de 24 horas |
bash
curl -X POST https://sua-empresa.konversia.com.br/api/v1/messages/text \
-H "Authorization: Bearer $KONVERSIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cobranca-8842" \
-d '{
"to": "5511999998888",
"text": "Seu boleto está disponível, vencimento dia 30."
}'json
{
"message_id": "b8f0a6b2-6e2b-4b39-9e2f-6c5e9c9e9a01",
"status": "queued",
"conversation_id": 918,
"thread_id": 2231
}message_id é um identificador de correlação, não o id da mensagem no banco: a mensagem só passa a existir de fato quando a fila processa o envio. Use esse mesmo valor em GET /v1/messages/{id}.
Enviar mídia (em duas etapas)
O envio de mídia é feito em duas etapas: primeiro sobe o arquivo, depois manda a mensagem referenciando o id recebido. O Konversia nunca aceita uma URL externa para buscar o arquivo: aceitar isso abriria uma forma de fazer o próprio servidor buscar qualquer endereço escondido atrás de um link, inclusive um endereço interno, uma técnica de ataque conhecida. Subir o arquivo diretamente é a defesa mais simples contra isso.
1. Subir o arquivo
POST /v1/files (multipart/form-data), escopo files:write.
| Campo | Tipo | Obrigatório |
|---|---|---|
file | arquivo | sim |
Limite de 15 MB por arquivo. O tipo é identificado pelo conteúdo real do arquivo, nunca pela extensão do nome nem pelo Content-Type declarado; só os tipos abaixo são aceitos:
imagem (jpg, png, gif, webp), vídeo (mp4, webm, 3gp), áudio (mp3, aac, ogg, wav), PDF, Word (doc, docx), Excel (xls, xlsx), CSV, texto simples e zip.
bash
curl -X POST https://sua-empresa.konversia.com.br/api/v1/files \
-H "Authorization: Bearer $KONVERSIA_API_KEY" \
-F "file=@boleto-8842.pdf"json
{
"id": 5931,
"name": "boleto-8842.pdf",
"mime": "application/pdf",
"size": 88213,
"type": "document",
"created_at": "2026-08-27T14:02:11+00:00"
}GET /v1/files (escopo files:read) lista os arquivos já no repositório da empresa, paginado (per_page, até 100), para reaproveitar o id de um arquivo enviado antes, pela API ou pela tela.
2. Enviar referenciando o id
POST /v1/messages/media
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Telefone do destinatário |
file_id | integer | sim | id devolvido pelo upload, da própria empresa |
caption | string | não | Legenda, até 1024 caracteres |
channel_id | integer | não | Ver Para onde a mensagem vai |
contact_name | string | não | Até 255 caracteres |
conversation_status | string | não | Ver Para onde a mensagem vai |
fallback_template | objeto | não | Ver A janela de 24 horas; fora da janela, este endpoint sempre recusa com media_requires_active_window |
bash
curl -X POST https://sua-empresa.konversia.com.br/api/v1/messages/media \
-H "Authorization: Bearer $KONVERSIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cobranca-8842" \
-d '{
"to": "5511999998888",
"file_id": 5931,
"caption": "Segue o boleto referente à fatura de agosto."
}'A resposta tem o mesmo formato do envio de texto: message_id, status: "queued", conversation_id e thread_id.
Um file_id que não existe, ou que pertence a outra empresa, é recusado com file_not_found (404): a resposta nunca confirma se aquele id existe em outro lugar.
Enviar template
POST /v1/messages/template
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
to | string | sim | Telefone do destinatário |
template_name | string | sim | Nome do template |
template_language | string | sim | Idioma aprovado do template |
variables | array de string | não | Valores posicionais, na ordem das variáveis do template |
channel_id | integer | não | Ver Para onde a mensagem vai |
contact_name | string | não | Até 255 caracteres |
conversation_status | string | não | Ver Para onde a mensagem vai |
O template precisa estar aprovado no canal resolvido para este envio: um template aprovado só em outra linha da empresa é recusado com template_not_approved (422), mesmo aparecendo em GET /v1/templates para a empresa.
A quantidade de itens em variables precisa bater exatamente com o que o template exige, somando cabeçalho, corpo e botões dinâmicos, não só o texto do corpo. Uma quantidade errada é recusada com template_variables_mismatch (422), que devolve expected e received em error.details.
bash
curl -X POST https://sua-empresa.konversia.com.br/api/v1/messages/template \
-H "Authorization: Bearer $KONVERSIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cobranca-8842-template" \
-d '{
"to": "5511999998888",
"template_name": "aviso_boleto",
"template_language": "pt_BR",
"variables": ["1234"]
}'Limitação conhecida: template com mídia dinâmica no cabeçalho
Um template cujo cabeçalho é imagem, vídeo ou documento escolhido em cada envio (mídia dinâmica) não pode ser enviado por este endpoint hoje: não existe campo para informar qual arquivo usar no cabeçalho daquele disparo em particular. Enviar esse template é recusado de cara com 422 e o código template_requires_header_media, antes de qualquer tentativa de entrega.
Um template cujo cabeçalho de mídia é fixo, o mesmo arquivo sempre, configurado uma vez na tela, não tem essa limitação e pode ser enviado normalmente.
GET /v1/templates ainda não expõe se um template exige mídia dinâmica no cabeçalho: se não tiver certeza, tente o envio e trate o 422, ou pergunte ao dono da empresa, que vê essa informação na tela de templates do painel.
Listar templates aprovados
GET /v1/templates, escopo templates:read.
json
{
"data": [
{
"id": 12,
"name": "aviso_boleto",
"language": "pt_BR",
"category": "utility",
"body": "Seu boleto de {{1}} está disponível.",
"variable_count": 1
}
]
}variable_count soma cabeçalho, corpo e botões dinâmicos; body mostra só o texto do corpo. Um template com variável no cabeçalho ou em um botão, por exemplo, tem variable_count maior do que a quantidade de visíveis em body.
Listar canais
GET /v1/channels, escopo channels:read.
json
{
"data": [
{
"id": 7,
"name": "Comercial",
"phone_number": "5511999990000",
"provider": "cloud_api",
"status": "connected",
"default_department": { "id": 3, "name": "Vendas" },
"capabilities": { "canEdit": false, "canRevoke": false, "supportsTemplates": true }
}
]
}capabilities varia por provedor: um canal QR Code e um canal da API oficial não suportam exatamente as mesmas coisas.
Consultar o status
GET /v1/messages/{id}, escopo messages:read. {id} é sempre o message_id devolvido no aceite (202), nunca outro identificador.
bash
curl https://sua-empresa.konversia.com.br/api/v1/messages/b8f0a6b2-6e2b-4b39-9e2f-6c5e9c9e9a01 \
-H "Authorization: Bearer $KONVERSIA_API_KEY"json
{
"id": "b8f0a6b2-6e2b-4b39-9e2f-6c5e9c9e9a01",
"status": "delivered",
"sent_at": "2026-08-27T14:03:02+00:00",
"delivered_at": "2026-08-27T14:03:05+00:00",
"read_at": null,
"reason": null
}Valores possíveis de status, na ordem em que costumam acontecer:
| Status | Significado |
|---|---|
queued | Aceito, ainda não processado pela fila |
pending | Processado, aguardando confirmação do WhatsApp |
sent | Confirmado pelo WhatsApp |
delivered | Entregue no aparelho do destinatário |
read | Lido pelo destinatário |
failed | Não foi entregue; reason traz o motivo |
skipped | Não chegou a ser enviado, por exemplo o contato foi bloqueado, o template deixou de estar aprovado entre o aceite e o processamento, ou caiu no guarda de mensagem duplicada |
Um id desconhecido para a empresa da chave devolve not_found (404), o mesmo comportamento de um id de outra empresa: a resposta nunca confirma que aquele id existe em outro lugar.
Exemplo completo: boleto em PDF
O caso que motivou o envio de mídia em duas etapas: um sistema de cobrança mandando o boleto do mês.
bash
# 1. Sobe o arquivo e guarda o id devolvido
FILE_ID=$(curl -s -X POST https://sua-empresa.konversia.com.br/api/v1/files \
-H "Authorization: Bearer $KONVERSIA_API_KEY" \
-F "file=@boleto-8842.pdf" | jq -r '.id')
# 2. Envia a mídia com legenda, referenciando o id
curl -X POST https://sua-empresa.konversia.com.br/api/v1/messages/media \
-H "Authorization: Bearer $KONVERSIA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cobranca-8842" \
-d "{
\"to\": \"5511999998888\",
\"file_id\": $FILE_ID,
\"caption\": \"Segue o boleto referente à fatura de agosto, vencimento dia 30.\"
}"Relacionado
- Autenticação
- Erros
- Limites de envio (guia do produto)
