Enviar um modelo do seu próprio sistema

Uma chamada de API envia um modelo aprovado do WhatsApp para um número — a partir de um webhook, de um cron ou de um script. A chave, o id do modelo, a requisição, cada recusa e as duas coisas para levar em conta no desenho.

Atualizado

Alguns modelos não são enviados por pessoa nenhuma. A Lavanderia Tidewater quer que a confirmação de coleta saia no instante em que o sistema de agendamento confirma um horário — ninguém deveria ter que abrir a caixa de entrada para isso. Para isso existe um endpoint:

POST https://api.omniocto.com/api/wa-templates/{id}/send

Ele envia um modelo aprovado para um número de telefone. Chame-o a partir de um handler de webhook ou de um script — “quando um pedido for despachado, envie o modelo de coleta”. Como é um modelo, a janela de 24 horas não se aplica (por quê).

Obtenha uma chave de API

As chaves ficam em /settings/api-keys — no menu da engrenagem, em Administração e sistema → Chaves de API.

  1. Clique em Criar chave. A caixa de diálogo, Criar chave de API, pede um Nome (opcional) — algo como “ex.: pipeline de CI”; dê a ela o nome do sistema que vai usá-la.
  2. Chave de API criada mostra a chave uma única vez, com “Copie esta chave agora — você não poderá vê-la novamente.” Guarde-a onde seu sistema guarda segredos.

A lista mostra a data de Último uso de cada chave, e Revogar chave a interrompe na hora: “Qualquer integração que a use vai falhar.” Uma chave por integração torna isso seguro.

Envie a chave em toda chamada como Authorization: Bearer <key>.

Encontre o id do modelo

Abra /templates/whatsapp e clique no modelo. O painel abre e o endereço da página ganha &template=wt-… — esse id curto wt- é o que vai no caminho. (O UUID do modelo também funciona.)

O mesmo painel diz o que a chamada precisa preencher:

  • Variáveis — as variáveis do corpo: ou “2 variáveis posicionais” ({{1}}, {{2}}), ou Nomeado: seguido dos nomes.
  • Cabeçalho — o texto dele, ou [IMAGE], [VIDEO] ou [DOCUMENT] para um cabeçalho de mídia.
  • Botões — listados em ordem, cada um com seu tipo ([URL], [COPY_CODE], [QUICK_REPLY]). O primeiro é o índice 0.

A partir de um script, GET /api/wa-templates?status=approved lista os modelos aprovados com shortId, name, language e category.

Só um modelo aprovado pode ser enviado; qualquer outro status é recusado.

A requisição

O corpo tem dois campos:

CampoO que é
toO número de telefone, em E.164 — +14255550123
componentsO próprio array template.components da Meta, repassado como está. Omita-o para um modelo sem variáveis

components é o mesmo array que você enviaria à Cloud API da Meta, então a documentação da Meta sobre ele se aplica. Em resumo:

  • Cabeçalho — um parâmetro: text para um cabeçalho de texto com uma variável, ou image, video ou document com um link https://.
  • Corpo — parâmetros text. Para um modelo posicional ({{1}}, {{2}}), envie-os em ordem e sem nomes. Para um modelo nomeado ({{customer_name}}), dê a cada parâmetro o seu parameter_name. Nunca misture os dois. Um modelo criado pelo Octo é posicional aqui, quaisquer que tenham sido os nomes que você deu.
  • Botões — um componente por botão que recebe um valor, com sub_type e index (a posição dele na lista de botões do modelo):
    • url — um parâmetro text: a parte do link que varia.
    • copy_code — o código, como o { "type": "coupon_code", "coupon_code": "FRESH20" } da Meta ou como { "type": "text" }. Até 15 caracteres.
    • quick_reply — opcional; um parâmetro { "type": "payload" } se você quiser receber um payload de volta quando ele for tocado.

Um exemplo completo

O modelo de coleta da Lavanderia Tidewater, pickup_confirmation, tem o corpo Hi {{1}}, your pickup is confirmed for {{2}}. Reply here to change it. e um botão Track pickup cujo link termina no número do pedido:

curl -X POST "https://api.omniocto.com/api/wa-templates/wt-a1b2c3d4e5/send" \
  -H "Authorization: Bearer $OMNI_OCTO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14255550123",
    "components": [
      { "type": "body", "parameters": [
        { "type": "text", "text": "Ana" },
        { "type": "text", "text": "Tuesday, 9–11am" }
      ] },
      { "type": "button", "sub_type": "url", "index": "0",
        "parameters": [{ "type": "text", "text": "TW-20931" }] }
    ]
  }'

Se o modelo usasse variáveis nomeadas, o corpo daria nome a cada valor:

{ "type": "body", "parameters": [
  { "type": "text", "parameter_name": "customer_name", "text": "Ana" },
  { "type": "text", "parameter_name": "pickup_slot", "text": "Tuesday, 9–11am" }
] }

Um sucesso é 201:

{
  "messageId": "6f1c…",
  "conversationId": "3a9e…",
  "conversationShortId": "cv-9f8e7d6c5b",
  "externalId": "wamid.HBgL…",
  "isNewConversation": false,
  "template": { "id": "…", "shortId": "wt-a1b2c3d4e5", "name": "pickup_confirmation", "language": "en_US" }
}

externalId é o id com que a Meta vai informar entrega e leitura; esses avisos chegam à conversa depois. 201 significa que a Meta aceitou o envio, não que o telefone recebeu. Guarde conversationShortId — você vai precisar dele mais abaixo.

Onde a mensagem aparece

Em /conversations, na conversa com esse número no número de WhatsApp do modelo — a mesma conversa em que chegam as mensagens do cliente:

  • Se já existe uma conversa com esse número, o modelo entra nela — e se ela estava fechada, é reaberta. isNewConversation é false.
  • Se não, uma nova conversa é aberta (e um contato é criado para um número desconhecido). isNewConversation é true.
  • O balão mostra o corpo com seus valores, com a etiqueta Modelo · e o nome do modelo.

Quando o cliente responde, a resposta chega a essa mesma conversa — e um envio pela API não passa a conversa para uma pessoa. Uma conversa que já existia continua com quem a atendia; uma fechada ou nova fica ativa com o agente de IA que responde naquele número de WhatsApp. Então esse agente, se o número tiver um, responde à resposta, a menos que uma pessoa tenha assumido a conversa em /conversations.

Quando um envio é recusado

StatuscodeSignificado
400—to não está em E.164, ou components não tem um formato que a Meta aceite; o corpo lista os problemas
402billing_not_eligibleO espaço de trabalho ainda não pode enviar mensagens de saída
404template_not_foundNão há modelo com esse id neste espaço de trabalho
409blocked_dncO número cancelou o recebimento. A mensagem fica na conversa como bloqueada; nada chega à Meta
422template_not_approvedA Meta não aprovou o modelo; status diz em que ponto ele está
422template_components_mismatchcomponents não encaixa no modelo. error aponta o problema
422channel_unavailableO número de WhatsApp do modelo foi removido ou desativado
422channel_pending_verification, workspace_suspended, route_blockedUma verificação de envio no número ou no espaço de trabalho recusou. A mensagem fica na conversa como bloqueada ou com falha
429workspace_rate_limitedSeu espaço de trabalho passou da taxa neste endpoint. Nada é gravado
429rate_limitedA janela de rajada do número de WhatsApp está cheia — espere de segundos a minutos
429daily_limit_reachedO limite diário de modelos do número acabou — a espera pode ser de horas
502send_failedA Meta rejeitou o envio; error traz a mensagem da Meta. A mensagem fica na conversa marcada como falha

Todo 429 traz um cabeçalho Retry-After e um retryAfterSec equivalente no corpo. Espere esse tempo e repita a mesma chamada: nenhuma mensagem foi enviada, gravada ou cobrada da primeira vez. (Um primeiro envio para um número novo que o limite do número recusa já abriu a conversa, vazia; o corpo traz o conversationShortId dela.)

As duas famílias de 429 são limites diferentes. Seu espaço de trabalho pode chamar este endpoint 300 vezes por minuto por padrão, em rajadas de até 10 por segundo; essa taxa conta só chamadas de API, e o suporte do Omni Octo a define por espaço de trabalho. O número de WhatsApp tem sua própria cota de envio, compartilhada com a caixa de entrada, o Octo e as campanhas — com que rapidez um número pode enviar explica.

Divergências não enviam nada

A chamada confere components com o modelo antes de gravar qualquer coisa. Um valor faltando, um sobrando, um nome desconhecido, o tipo errado de cabeçalho, um parâmetro num botão que não aceita — cada um é um 422 template_components_mismatch, sem mensagem gravada e nada enviado. O error diz exatamente o que está errado:

body expects 2 parameters, got 1
body is missing variable "pickup_slot"
button 0 is a QUICK_REPLY button, got url
button 0 (a URL button) is missing its url parameter

Então uma divergência pode ser corrigida e reenviada com segurança.

A chamada não é idempotente

Toda chamada bem-sucedida envia — e cobra — um modelo. Não há chave de idempotência. Se uma chamada estourar o tempo limite ou a conexão cair, você não sabe se ela saiu, e repetir às cegas pode enviá-la duas vezes.

Antes de repetir depois de um tempo limite, confira a conversa —

GET /api/conversations/{conversationShortId}

com o id de um envio anterior para esse número, ou abra a conversa em /conversations. Só reenvie se o modelo não estiver lá.

As recusas acima são diferentes: um 4xx diz que o envio não aconteceu.

O que este endpoint não faz

  • Enviar texto livre. Ele envia só modelos aprovados — para uma resposta dentro da janela, use a caixa de entrada.
  • Enviar para uma lista. Uma chamada é um número; as campanhas cuidam de listas.
  • Passar por cima da Meta. A Meta ainda pode recusar um modelo de marketing depois de um 201 — veja por quê.

Para onde ir depois