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.
- 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.
- 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 índice0.
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:
| Campo | O que é |
|---|---|
to | O número de telefone, em E.164 — +14255550123 |
components | O 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:
textpara um cabeçalho de texto com uma variável, ouimage,videooudocumentcom umlinkhttps://. - 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 seuparameter_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_typeeindex(a posição dele na lista de botões do modelo):url— um parâmetrotext: 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
| Status | code | Significado |
|---|---|---|
| 400 | — | to não está em E.164, ou components não tem um formato que a Meta aceite; o corpo lista os problemas |
| 402 | billing_not_eligible | O espaço de trabalho ainda não pode enviar mensagens de saída |
| 404 | template_not_found | Não há modelo com esse id neste espaço de trabalho |
| 409 | blocked_dnc | O número cancelou o recebimento. A mensagem fica na conversa como bloqueada; nada chega à Meta |
| 422 | template_not_approved | A Meta não aprovou o modelo; status diz em que ponto ele está |
| 422 | template_components_mismatch | components não encaixa no modelo. error aponta o problema |
| 422 | channel_unavailable | O número de WhatsApp do modelo foi removido ou desativado |
| 422 | channel_pending_verification, workspace_suspended, route_blocked | Uma verificação de envio no número ou no espaço de trabalho recusou. A mensagem fica na conversa como bloqueada ou com falha |
| 429 | workspace_rate_limited | Seu espaço de trabalho passou da taxa neste endpoint. Nada é gravado |
| 429 | rate_limited | A janela de rajada do número de WhatsApp está cheia — espere de segundos a minutos |
| 429 | daily_limit_reached | O limite diário de modelos do número acabou — a espera pode ser de horas |
| 502 | send_failed | A 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
- Traga seus modelos para o Omni Octo — o modelo precisa estar aprovado e sincronizado antes.
- Com que rapidez um número pode enviar
— a cota por número por trás de
rate_limitededaily_limit_reached.