Skip to content

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.

EndpointEnvia
POST /v1/messages/textTexto livre
POST /v1/messages/mediaMídia (imagem, vídeo, áudio ou documento), com legenda opcional
POST /v1/messages/templateUm 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 com channel_not_found e 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_progress ou resolved; padrão pending).

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 com outside_24h_window (422), e a resposta já traz em error.details.approved_templates a 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

CampoTipoObrigatórioDescrição
tostringsimTelefone do destinatário
textstringsimAté 4096 caracteres
channel_idintegernãoVer Para onde a mensagem vai
contact_namestringnãoAté 255 caracteres
conversation_statusstringnãoVer Para onde a mensagem vai
fallback_templateobjetonãoVer 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.

CampoTipoObrigatório
filearquivosim

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

CampoTipoObrigatórioDescrição
tostringsimTelefone do destinatário
file_idintegersimid devolvido pelo upload, da própria empresa
captionstringnãoLegenda, até 1024 caracteres
channel_idintegernãoVer Para onde a mensagem vai
contact_namestringnãoAté 255 caracteres
conversation_statusstringnãoVer Para onde a mensagem vai
fallback_templateobjetonãoVer 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

CampoTipoObrigatórioDescrição
tostringsimTelefone do destinatário
template_namestringsimNome do template
template_languagestringsimIdioma aprovado do template
variablesarray de stringnãoValores posicionais, na ordem das variáveis do template
channel_idintegernãoVer Para onde a mensagem vai
contact_namestringnãoAté 255 caracteres
conversation_statusstringnãoVer 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:

StatusSignificado
queuedAceito, ainda não processado pela fila
pendingProcessado, aguardando confirmação do WhatsApp
sentConfirmado pelo WhatsApp
deliveredEntregue no aparelho do destinatário
readLido pelo destinatário
failedNão foi entregue; reason traz o motivo
skippedNã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