> ## Documentation Index
> Fetch the complete documentation index at: https://docs.octasend.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Enviar mensagens

> Endpoint send-message: parâmetros, canais, regras de envio, resposta, envio em lote, limite por minuto, limite diário, descadastro e erros

O endpoint [`POST /send-message`](https://docs.octasend.com.br/api-reference/send-message) dispara uma [comunicação](https://docs.octasend.com.br/plataforma/comunicacoes.md) cadastrada no painel para um destinatário — em todos os canais ativos dela ou só nos que você escolher. O conteúdo de cada canal vem da versão publicada do template da comunicação; a sua integração só informa quem recebe e os valores das variáveis.

Antes de começar, obtenha um access token em [Autenticação](https://docs.octasend.com.br/api-reference/autenticacao.md).

```http theme={null}
POST https://api.octasend.com.br/functions/v1/send-message
Authorization: Bearer at_f4a92b...
Content-Type: application/json

{
  "communication_slug": "boas-vindas",
  "recipient": {
    "email": "usuario@exemplo.com",
    "phone": "+5511999990000",
    "user_id": "id-do-usuario-no-seu-sistema"
  },
  "variables": {
    "nome": "Ricardo",
    "link": "https://exemplo.com/ativar/xyz"
  },
  "language": "pt-BR",
  "channels": ["email", "whatsapp", "web_push", "in_app"],
  "tag": ["onboarding", "junho-2024"]
}
```

O `channels` do exemplo restringe o envio a esses quatro canais, cada um com o dado de `recipient` que ele usa: e-mail (`email`), WhatsApp (`phone`), Web Push e In-App (`user_id`). Sem `channels`, a mensagem sai em todos os canais ativos da comunicação para os quais o `recipient` tem dado.

## Parâmetros do body

| Campo | Tipo | Obrigatório | Descrição |
| - | - | - | - |
| `communication_slug` | string | Sim | Slug da comunicação, como aparece em **Comunicações**. A comunicação precisa estar ativa |
| `recipient` | object | Depende do canal | Dados do destinatário. Informe os campos dos canais que vão ser usados (tabela abaixo) |
| `variables` | object | Não | Valores para as `{{variáveis}}` do template. Ex.: `{"nome": "Ricardo"}`. Envie os valores como texto |
| `language` | string | Não | Idioma preferido do destinatário (ex.: `pt-BR`, `en`). Veja [como o idioma é escolhido](#idioma-do-template) |
| `channels` | string\[] | Não | Restringe o envio a estes canais. Ex.: `["email", "whatsapp"]`. Sem ele, a mensagem sai em todos os canais ativos da comunicação |
| `tag` | string ou string\[] | Não | Uma ou mais tags para agrupar os envios. Ex.: `"onboarding"` ou `["onboarding", "black-friday"]`. Espaços nas pontas são removidos e tags vazias, descartadas. Filtrável em [Consultar logs](https://docs.octasend.com.br/api-reference/consultar-logs.md) |
| `respect_opt_out` | boolean | Não | Padrão `false`. Com `true`, o canal (e-mail, WhatsApp ou SMS) de que o contato se descadastrou é pulado e volta como `skipped`. Veja [Descadastro de contatos](#descadastro-de-contatos) |
| `external_id` | string | Não | Id do pedido/registro no seu sistema (texto, máx. 255 caracteres). Volta em todo callback do [Webhook de status](https://docs.octasend.com.br/api-reference/webhook-de-status.md). No lote, vai em cada item de `messages` |

### Campos do destinatário

| Campo | Usado por | Descrição |
| - | - | - |
| `recipient.email` | E-mail, Slack | E-mail do destinatário. No Slack, abre uma mensagem direta com o usuário dono desse e-mail (exige Bot Token no canal) |
| `recipient.phone` | WhatsApp, SMS, Telegram | Telefone com código do país. No Telegram, localiza quem já compartilhou esse número com o bot. Veja [como um número sem `+` é lido](#telefone) |
| `recipient.user_id` | Web Push, In-App, Telegram | Identificador do usuário **no seu sistema** — o mesmo usado no registro do Web Push, no widget In-App ou no link de início do bot do Telegram |
| `recipient.fcm_token` | Web Push | Token FCM do navegador, para enviar sem depender do registro por `user_id` |
| `recipient.telegram_chat_id` | Telegram | Chat ID do Telegram |
| `recipient.slack_user_id` | Slack | ID do usuário no Slack, para mensagem direta |
| `recipient.slack_channel` | Slack | Canal do Slack (ex.: `#geral` ou o ID do canal) |
| `recipient.push_token` | Push mobile | Endpoint do dispositivo no Amazon SNS |

## Canais disponíveis

| Canal | Valor em `channels` | Dados do destinatário necessários |
| - | - | - |
| E-mail | `email` | `recipient.email` |
| WhatsApp | `whatsapp` | `recipient.phone` (celular brasileiro) |
| SMS | `sms` | `recipient.phone` no formato internacional (ex.: `+5511999990000`) |
| Telegram | `telegram` | `recipient.telegram_chat_id`, `recipient.user_id` ou `recipient.phone`, nessa ordem de prioridade |
| Slack | `slack` | `recipient.slack_channel`, `recipient.slack_user_id` ou `recipient.email` — ou nenhum, se o canal tiver canal padrão ou Incoming Webhook configurado |
| Web Push | `web_push` | `recipient.user_id` ou `recipient.fcm_token` |
| In-App | `in_app` | `recipient.user_id` |
| Push mobile | `push` | `recipient.push_token` |

Todo canal, exceto In-App, precisa estar configurado em **Configurações → Canais**. Sem configuração, o envio por aquele canal nem começa: o resultado volta como `skipped`, com o erro `Canal não configurado` e `reason_code: "channel_not_configured"`, sem `log_id` e sem registro em **Logs**. Os outros canais da comunicação seguem normalmente. O In-App não tem configuração, mas respeita a chave **Canal habilitado** da página do canal: desligada, o resultado volta como `skipped` com o erro `Canal in-app desabilitado`. Veja os guias de [WhatsApp](https://docs.octasend.com.br/guias/whatsapp.md), [E-mail](https://docs.octasend.com.br/guias/email.md), [Web Push](https://docs.octasend.com.br/guias/web-push.md), [Slack](https://docs.octasend.com.br/guias/slack.md) e [Telegram](https://docs.octasend.com.br/guias/telegram.md).

## Regras de envio

### Quais canais recebem

* A comunicação é procurada pelo `communication_slug` entre as comunicações **ativas** da conta. Se não existir ou estiver inativa, a resposta é `404 Communication not found`. Se você desativar a comunicação no painel, as chamadas com o slug dela passam a receber esse `404`; se trocar o slug, as chamadas com o slug antigo também.
* Sem `channels`, o envio sai em todos os canais ativos da comunicação. Com `channels`, só nos canais que estão na lista **e** ativos na comunicação — um canal pedido que a comunicação não tem ativo é ignorado.
* **Destinatário sem os dados de um canal:**
  * sem `channels`, o canal é pulado em silêncio — não aparece em `results` nem nos logs. Assim uma comunicação multicanal pode ser enviada a quem só tem e-mail, por exemplo;
  * com `channels`, o canal aparece em `results` com status `skipped` e um `error` que diz qual campo faltou — exceto o Slack: se o canal Slack não tiver canal padrão nem Incoming Webhook e o destinatário vier sem `slack_channel`, `slack_user_id` e `email`, ele é pulado em silêncio mesmo quando pedido em `channels`.

> [!NOTE]
>
> Se nenhum canal tiver dados do destinatário e você não informou `channels`, a resposta volta com `results` vazio: nada foi enviado nem registrado.

### Versão publicada do template

O envio usa sempre a versão **publicada** de cada template. No editor do painel, o que você grava com **Salvar rascunho** não sai pela API até você clicar em **Publicar**. Uma versão de idioma que ainda não foi publicada não entra na escolha do idioma. O **Enviar teste** do editor é o único envio que usa o rascunho.

### Idioma do template

Cada canal da comunicação pode ter um template por idioma. A plataforma escolhe o primeiro que existir nesta ordem:

1. o idioma enviado em `language` (ex.: `pt-BR`);
2. a base desse idioma (ex.: `pt`);
3. o **Idioma padrão** da conta;
4. o **Idioma de fallback** da conta;
5. `en`.

Se nenhum casar e o canal tiver **um único** template ativo, ele é usado mesmo assim. Com dois ou mais templates e nenhum casando, o canal fica `skipped` com o erro `No template found for any language` — a plataforma não escolhe um idioma ao acaso. O idioma padrão e o de fallback ficam em [Configurações](https://docs.octasend.com.br/plataforma/configuracoes.md).

### Variáveis

* Use `{{nome_da_variavel}}` no template. Nomes com ponto também funcionam (ex.: `{{pedido.numero}}`).
* Variável que o template usa mas não veio em `variables` fica no texto como está (ex.: `{{nome}}`) — confira os nomes antes de enviar.
* No corpo HTML do e-mail, o valor das variáveis é tratado como texto: HTML enviado dentro de uma variável aparece como texto, e quebras de linha viram quebras de linha no e-mail.
* No WhatsApp e no SMS, o texto final (template com as variáveis já preenchidas) acima de 3.800 caracteres é cortado e termina com `[…continua]`.

### Telefone

Envie `recipient.phone` sempre com `+` e o código do país (ex.: `+5511999990000`). Um número sem o `+` é lido assim no WhatsApp, no cadastro do contato e na conferência do descadastro:

| Número sem `+` | Como é lido |
| - | - |
| 10 dígitos | Fixo brasileiro com DDD: recebe o `55` |
| 11 dígitos com `9` na terceira posição | Celular brasileiro com DDD: recebe o `55` |
| Outros, com 11 dígitos ou mais | Número que já traz o código do país |
| Menos de 10 dígitos | Não é um telefone válido |

No SMS, o número vai ao provedor como você enviou: use o formato internacional.

### Contatos

Os envios para um destinatário com e-mail ou telefone criam ou atualizam o contato correspondente em [Contatos](https://docs.octasend.com.br/plataforma/contatos.md). Quando `recipient.email` é enviado, o nome do contato é preenchido com o valor da variável `nome` (ou `name`), se ela vier no envio, e o telefone, com `recipient.phone` na regra da seção [Telefone](#telefone). Um contato removido no painel não volta por causa de um envio: ele continua removido.

## Resposta

```json theme={null}
{
  "success": true,
  "results": [
    {
      "channel": "email",
      "status": "sent",
      "provider_message_id": "010f018e...",
      "log_id": "3f1c2b9e-..."
    },
    {
      "channel": "whatsapp",
      "status": "skipped",
      "error": "Canal não configurado",
      "reason_code": "channel_not_configured"
    }
  ]
}
```

| Campo de `results[]` | Descrição |
| - | - |
| `channel` | Canal do resultado |
| `status` | Resultado do envio naquele canal (tabela abaixo) |
| `message_id` | Id da mensagem na OctaSend, o mesmo `data.message_id` do [Webhook de status](https://docs.octasend.com.br/api-reference/webhook-de-status.md). `null` quando a falha acontece antes de o registro ser criado |
| `provider_message_id` | Identificador devolvido pelo provedor, quando há |
| `error` | Motivo, quando o status é `failed` ou `skipped` |
| `error_code` | Código fixo do motivo, para a sua integração não depender do texto de `error`. Hoje só aparece como `daily_limit_reached` (veja **Limite diário da conta**, abaixo) |
| `reason_code` | Código fixo do motivo de um canal `skipped`: `channel_not_configured` (o canal não está configurado na conta) ou `contato_descadastrado`, com `respect_opt_out: true` (veja **Descadastro de contatos**, abaixo) |
| `log_id` | Identificador do registro em **Logs**. Ausente quando o canal foi pulado antes de gerar registro |
| `note` | Só no WhatsApp enfileirado: `Message queued (will be sent in intervals)` |

| Status | Significado |
| - | - |
| `sent` | Enviada — o provedor aceitou a mensagem |
| `queued` | Enfileirada — no WhatsApp por conexão Evolution (QR Code) com fila ativa, a mensagem sai da fila em intervalos |
| `failed` | Falhou — veja `error` |
| `skipped` | Ignorada — o canal não está configurado na conta, faltou dado do destinatário, não há template no idioma, o número não tem WhatsApp, o canal In-App está desabilitado ou, com `respect_opt_out: true`, o contato se descadastrou do canal |

> [!WARNING]
>
> HTTP `200` significa que a requisição foi aceita, não que a mensagem foi entregue. Confira o `status` de cada item de `results`. O que acontece depois do envio (entrega, leitura, clique, recusa do provedor) aparece em **Logs** e em [Consultar logs](https://docs.octasend.com.br/api-reference/consultar-logs.md) — ou chega até você por [Webhook de status](https://docs.octasend.com.br/api-reference/webhook-de-status.md).

## Envio em lote

Envie de **1 a 100 mensagens** numa única chamada com o campo `messages`. Cada item aceita os mesmos campos do envio único (`communication_slug`, `recipient`, `variables`, `language`, `channels`, `tag`, `respect_opt_out`, `external_id`) e pode usar uma comunicação diferente.

```http theme={null}
POST https://api.octasend.com.br/functions/v1/send-message
Authorization: Bearer at_f4a92b...
Content-Type: application/json

{
  "messages": [
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["email"],
      "recipient": { "email": "joao@exemplo.com" },
      "variables": { "nome": "João" },
      "tag": "campanha-julho"
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["email", "whatsapp"],
      "recipient": { "email": "maria@exemplo.com", "phone": "+5511988880000" },
      "variables": { "nome": "Maria" }
    }
  ]
}
```

Sem `channels`, o item sai em todos os canais ativos da comunicação para os quais o `recipient` tem dado.

### Exemplos por canal

Um item para cada canal, com o `recipient` que ele exige (veja [Canais disponíveis](#canais-disponíveis)). Os itens podem ser misturados no mesmo lote. Os exemplos supõem que a comunicação `boas-vindas` tem todos esses canais configurados; sem a configuração, o item daquele canal volta como `skipped`.

```json theme={null}
{
  "messages": [
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["email"],
      "recipient": { "email": "joao@exemplo.com" },
      "variables": { "nome": "João" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["whatsapp"],
      "recipient": { "phone": "+5511988880000" },
      "variables": { "nome": "Maria" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["sms"],
      "recipient": { "phone": "+5511977770000" },
      "variables": { "nome": "Carlos" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["telegram"],
      "recipient": { "telegram_chat_id": "123456789" },
      "variables": { "nome": "Ana" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["slack"],
      "recipient": { "slack_user_id": "U01ABCDEF" },
      "variables": { "nome": "Paulo" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["slack"],
      "recipient": { "slack_channel": "#geral" },
      "variables": { "nome": "Time" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["web_push"],
      "recipient": { "user_id": "usuario-123" },
      "variables": { "nome": "Bia" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["in_app"],
      "recipient": { "user_id": "usuario-456" },
      "variables": { "nome": "Leo" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["push"],
      "recipient": {
        "push_token": "arn:aws:sns:us-east-1:123456789012:endpoint/GCM/app/abc",
        "email": "rita@exemplo.com"
      },
      "variables": { "nome": "Rita" }
    },
    {
      "communication_slug": "boas-vindas",
      "language": "pt-BR",
      "channels": ["email", "whatsapp", "web_push", "in_app"],
      "recipient": {
        "email": "lia@exemplo.com",
        "phone": "+5511966660000",
        "user_id": "usuario-789"
      },
      "variables": { "nome": "Lia" },
      "tag": ["campanha-julho", "multicanal"]
    }
  ]
}
```

Regras do lote:

* Todo item precisa de `communication_slug`. Se algum não tiver, o lote inteiro é recusado com `400` e nada é enviado. O mesmo vale para um `respect_opt_out` que não seja `true` nem `false`.
* Um slug inexistente ou inativo **não** derruba o lote: aquele item volta com `"error": "Communication not found"` e os demais seguem.
* Com `respect_opt_out: true`, uma falha ao consultar o descadastro também fica só no item: ele volta com `error` e `"error_code": "opt_out_check_failed"`, nada é enviado para ele e os demais seguem. Reenvie só esse item.
* Para listas maiores que 100, divida em lotes e faça várias chamadas.

### Resposta do lote

```json theme={null}
{
  "success": true,
  "batch": true,
  "total": 3,
  "deferred": 1,
  "results": [
    {
      "communication_slug": "boas-vindas",
      "results": [{ "channel": "email", "status": "sent", "log_id": "..." }]
    },
    {
      "communication_slug": "boas-vindas-antiga",
      "error": "Communication not found"
    },
    {
      "communication_slug": "boas-vindas",
      "status": "queued",
      "deferred": true
    }
  ]
}
```

| Campo | Descrição |
| - | - |
| `total` | Quantidade de itens recebidos em `messages` |
| `deferred` | Quantos itens foram adiados pelo [limite por minuto](#limite-de-envio-por-minuto) |
| `results[]` | Um item por mensagem: com `results` por canal, com `error` (comunicação não encontrada ou, com `error_code: "opt_out_check_failed"`, falha ao consultar o descadastro) ou com `status: "queued"` e `deferred: true` (adiada) |

## Limite de envio por minuto

Cada conta tem um orçamento de mensagens por minuto — **60 por padrão**. Cada mensagem conta 1, qualquer que seja o número de canais; num lote, cada item conta 1. Para aumentar o limite, fale com a OctaSend.

O orçamento é da conta inteira: além das chamadas à API, as mensagens disparadas por automações, recuperação de carrinho e eventos das lojas integradas consomem do mesmo limite.

O excedente **não é recusado** — ele é adiado:

* **Envio único:** a resposta é `{"success": true, "deferred": true, "status": "queued"}`, sem `results`.
* **Lote:** os primeiros itens, até o que cabe no minuto, são processados na hora; os demais são adiados e aparecem no fim de `results` com `status: "queued"` e `deferred: true`.

A resposta adiada não traz `message_id` (o registro só nasce quando a mensagem é reprocessada) e devolve o `external_id` enviado. As mensagens adiadas saem nos minutos seguintes, conforme o orçamento libera, e o resultado de cada canal aparece em **Logs** — exceto os canais pulados por falta de dado do destinatário, que não geram registro, como no envio imediato. Use `tag` para encontrá-las depois em [Consultar logs](https://docs.octasend.com.br/api-reference/consultar-logs.md). Uma mensagem adiada que não consegue sair em cerca de 24 horas é descartada.

> [!WARNING]
>
> O adiamento acontece antes de a comunicação ser conferida: a resposta `queued` não valida o `communication_slug`. Se a comunicação não existir ou estiver inativa quando a mensagem sair da fila, ela é descartada sem registro em **Logs**. Confira o slug antes de disparar volumes grandes — por exemplo, com um envio que não seja adiado, que responde `404 Communication not found` quando a comunicação não existe.

## Limite diário da conta

Se a conta tiver um **Limite diário de mensagens** em [Configurações](https://docs.octasend.com.br/plataforma/configuracoes.md), cada envio a um canal conta 1: uma mensagem que sai por e-mail e WhatsApp conta 2. O dia vira à meia-noite, no fuso horário da conta; se o fuso da conta for inválido, vale o de `America/Sao_Paulo`. Com o campo vazio, não há limite.

O limite vale para a conta inteira: envios pela API (único e lote), campanhas, automações, eventos das lojas integradas, mensagens adiadas pelo limite por minuto e o **Enviar teste** do painel. Respostas do chatbot ou do atendente nas conversas não contam. Mensagens da plataforma, como o código de acesso ao painel, os convites de usuário, as boas-vindas e os alertas do [Token Control](https://docs.octasend.com.br/plataforma/token-control.md), também não contam nem são recusadas pelo limite.

Acima do limite, o canal não é chamado. A resposta continua HTTP `200` e o canal volta assim — no lote, dentro do item:

```json theme={null}
{
  "channel": "email",
  "status": "failed",
  "error": "Limite diário de mensagens atingido",
  "error_code": "daily_limit_reached",
  "log_id": "5b0e7a13-..."
}
```

* O envio recusado fica em **Logs** com status **Falhou** e esse motivo.
* Canais pulados antes do envio (por falta de dado do destinatário, sem template, sem configuração ou por descadastro) não contam.
* Aumentar o limite no mesmo dia libera o saldo novo na hora.
* Os envios do dia são contados mesmo sem limite. Se você definir o limite no meio do dia, os envios que já saíram hoje entram na conta.
* Em **Configurações**, o painel mostra quanto já foi usado no dia e avisa quando o limite é atingido.
* Campanhas não viram uma fila de falhas: quando a conta atinge o limite no meio do envio, a campanha fica **Pausada** e volta a enviar sozinha à meia-noite, no fuso da conta. Veja [Campanhas](https://docs.octasend.com.br/plataforma/campanhas.md#status-da-campanha).

## Descadastro de contatos

O contato pode se descadastrar por canal (e-mail, WhatsApp e SMS). O descadastro vale para as mensagens de **marketing**. As mensagens **transacionais**, sobre um pedido ou a conta do próprio contato, continuam chegando.

| Origem do envio | Respeita o descadastro? |
| - | - |
| `send-message` sem `respect_opt_out` ou com `false` | Não: é tratado como envio transacional e a mensagem sai normalmente |
| `send-message` com `respect_opt_out: true` | Sim |
| [Campanhas](https://docs.octasend.com.br/plataforma/campanhas.md) | Sim |
| [Automações](https://docs.octasend.com.br/plataforma/automacoes.md) iniciadas por segmento de contatos, por data do contato, pelo `flow-trigger` ou por carrinho abandonado | Sim |
| Carrinho abandonado configurado direto na integração da loja | Sim |
| Automações iniciadas por um pedido (criado, pago, enviado, cancelado, avaliação pós-compra) ou por um evento da loja de pedido, cliente ou produto | Não: a mensagem sai normalmente, sem o link de descadastro |
| Eventos de pedido configurados direto na integração da loja | Não |

Quando o descadastro é respeitado, o canal descadastrado não é enviado e fica em **Logs** com o status `skipped` e o motivo `Contato descadastrado`. Os outros canais da mesma mensagem seguem normalmente. Os e-mails de campanha e de automação de marketing levam no rodapé o link de descadastro.

### Respeitar o descadastro pela API

Envie `respect_opt_out: true` no corpo do `send-message` (no lote, em cada item de `messages`). O contato é reconhecido pelo `recipient.email`, sem diferenciar maiúsculas, e pelo `recipient.phone`. O canal descadastrado volta assim:

```json theme={null}
{
  "channel": "email",
  "status": "skipped",
  "error": "Contato descadastrado",
  "reason_code": "contato_descadastrado",
  "log_id": "7c1e0a52-..."
}
```

* Use `reason_code` para identificar o pulo sem depender do texto de `error`.
* Telegram, Slack, Web Push, In-App e Push mobile não têm descadastro e não são afetados.
* O campo vale também para mensagens adiadas pelo [limite por minuto](#limite-de-envio-por-minuto).
* Se a consulta ao descadastro falhar, nada é enviado para aquela mensagem e a resposta traz `"error_code": "opt_out_check_failed"`: no envio único, com HTTP `503`; no lote, só no item afetado, e os outros itens seguem. Reenvie a mensagem.

> [!WARNING]
>
> Os e-mails enviados pela API não levam o link de descadastro, nem com `respect_opt_out: true`. Se você usa a API para comunicação de marketing, oferecer o descadastro é responsabilidade do seu sistema: não chame o `send-message` para quem pediu para sair, ou envie `respect_opt_out: true` para a OctaSend pular os canais de que o contato se descadastrou. Veja o descadastro de cada contato em [Contatos](https://docs.octasend.com.br/plataforma/contatos.md).

## WhatsApp

### Número do destinatário

* O envio de WhatsApp pela API aceita **somente celular brasileiro**: DDI 55, DDD e número de 9 dígitos começando por 6, 7, 8 ou 9. Número fixo ou de outro país volta como `failed` com o erro `Invalid phone number`.
* Informe o número com `+55` (ex.: `+5511999990000`). Um celular de 11 dígitos sem DDI, com DDD e o `9` na frente do número, com ou sem máscara (ex.: `(11) 99999-0000`), recebe o `55` automaticamente. Veja [Telefone](#telefone).
* Número que não está registrado no WhatsApp volta como `skipped` com o erro `Número não está registrado no WhatsApp`.

### Método de envio

O método de envio (Meta Cloud API ou Evolution API) é definido na conexão do canal dentro da plataforma — a integração pela API não precisa especificá-lo. Veja o [guia de WhatsApp](https://docs.octasend.com.br/guias/whatsapp.md).

Pela Evolution API com a fila ligada, a mensagem entra numa fila e sai em intervalos: o resultado imediato é `queued`, e o resultado final aparece em **Logs**.

* A fila respeita o **Intervalo mínimo entre envios** da conta, em **Configurações → Canais → WhatsApp → Configurações Gerais**: de 1 a 60 segundos entre uma mensagem e a próxima. Em branco, a fila segue só o limite por minuto. O intervalo de uma conta não atrasa a fila das outras. Ao mudar o intervalo, as mensagens que já esperam na fila passam a seguir o ritmo novo.
* Quando o envio falha por um problema passageiro, como o número desconectado, a fila tenta de novo até o máximo de tentativas da conta. Sem novas tentativas configuradas, a primeira falha é final.
* Depois que a mensagem sai, o registro em **Logs** passa para **Entregue** e **Lida** conforme o WhatsApp confirma. Veja [Consultar logs](https://docs.octasend.com.br/api-reference/consultar-logs.md#status-de-um-envio).

### Sequência de mensagens

O template de WhatsApp pode ter uma sequência de mensagens, com uma espera configurável depois de cada uma (padrão: 1 segundo). A resposta traz um único resultado para o canal: `failed` se algum item falhou; senão `queued` se algum foi para a fila; senão `skipped` se nenhum item saiu porque o número não tem WhatsApp; senão `sent`. Cada pausa entre mensagens enviadas na hora é limitada a 30 segundos, e a soma das pausas de uma sequência, a 60 segundos — depois disso, os itens restantes seguem sem pausa.

### Mensagens ricas

Botões, listas, mídia, áudio e carrossel são configurados **no template da comunicação**, pelo editor do painel — o payload da API não muda: continue enviando `communication_slug`, `recipient.phone` e `variables`.

| Tipo | Comportamento |
| - | - |
| Texto (padrão) | Texto puro |
| Mídia | Imagem, vídeo ou documento com o texto como legenda |
| Áudio | Mensagem de voz; o texto, se houver, é enviado em seguida |
| Botões | Até 3 botões (resposta, link ou ligação), com até 20 caracteres de texto cada |
| Lista | Menu de opções com até 10 seções de até 10 itens cada |
| Carrossel | Até 10 cartões de mídia com legenda (carrossel emulado) |

As `{{variáveis}}` funcionam em todos os campos de texto do conteúdo rico (legendas, títulos, textos de botões e itens de lista).

> [!WARNING]
>
> Botões e listas dependem do aparelho do destinatário. Se o envio de botões ou lista falhar, a plataforma reenvia automaticamente como texto com as opções numeradas.

No método **Meta (Cloud API oficial)**, uma mensagem de texto simples, sozinha, usa o template aprovado configurado na conexão (campo **Template Name**), se houver um. Nesse caso:

* o destinatário recebe o texto do template aprovado na Meta, não o texto do template da comunicação;
* o idioma é o do campo **Template Language** da conexão (`pt_BR` quando vazio);
* os valores de `variables` preenchem os parâmetros `{{1}}`, `{{2}}`… do template aprovado na ordem numérica das chaves — use `"1"`, `"2"`… como nomes. Chaves não numéricas entram depois, em ordem alfabética. Todas as chaves enviadas em `variables` viram parâmetros, então envie só as que o template aprovado usa;
* o envio pode sair fora da janela de 24 horas.

Sem template aprovado configurado na conexão, o texto vai como mensagem livre. Conteúdo rico ou sequência com mais de uma mensagem também é enviado como mensagem livre. Mensagem livre só é entregue dentro da **janela de 24 horas** de atendimento (o destinatário precisa ter enviado uma mensagem recentemente). Fora da janela, a Meta rejeita o envio e o erro fica registrado no log.

Diferenças do método Meta em relação à Evolution:

| Recurso | Comportamento no método Meta |
| - | - |
| Botões | Até 3 botões de resposta nativos. Um único botão de link, sozinho, vira um botão de link nativo com o texto que você definiu. Não existe botão de ligação nativo nem combinação de link com resposta — nesses casos, os links e telefones são anexados como texto à mensagem |
| Lista | Máximo de **10 itens no total**, somando todas as seções. Acima disso, vira texto numerado |
| Carrossel | Emulado como sequência de mensagens de mídia |

## In-App

O canal In-App não tem configuração de credenciais: a notificação é gravada para o `recipient.user_id` e aparece no widget instalado no seu produto. Veja [Widget In-App](https://docs.octasend.com.br/api-reference/widget-in-app.md).

## Disparar uma automação pela API

Se a sua conta usa [Automações](https://docs.octasend.com.br/plataforma/automacoes.md) — módulo opcional, fale com a OctaSend para ativar —, um fluxo cujo início é **Chamada de API** pode ser disparado pelo endpoint `POST /flow-trigger`, com a mesma autenticação:

```http theme={null}
POST https://api.octasend.com.br/functions/v1/flow-trigger
Authorization: Bearer at_f4a92b...
Content-Type: application/json

{
  "trigger_slug": "cadastro-concluido",
  "recipient": { "email": "usuario@exemplo.com", "phone": "+5511999990000" },
  "variables": { "nome": "Ricardo" },
  "idempotency_key": "cadastro-12345"
}
```

* `trigger_slug` é o **Identificador do gatilho** definido no início do fluxo. Só fluxos ativos são disparados.
* `recipient` precisa de ao menos um identificador: `email`, `phone`, `user_id`, `telegram_chat_id`, `slack_user_id`, `slack_channel` ou `fcm_token`.
* `idempotency_key` é opcional. Chamadas repetidas com a mesma chave para o mesmo gatilho não iniciam uma nova execução — elas voltam contadas em `duplicated`. Sem a chave, cada chamada inicia uma execução nova.
* A resposta informa quantas execuções começaram e quantas foram ignoradas por repetição, ex.: `{"started": 1, "duplicated": 0}`. Mais de um fluxo ativo pode usar o mesmo identificador. Se nenhum fluxo ativo usa o `trigger_slug`, a resposta é `404`.
* Cada chamada consome 1 do [limite por minuto](#limite-de-envio-por-minuto). Aqui o excedente **não** é adiado: a resposta é `429` com `{"error": "rate_limited", "retry_after": 60}`.
* As mensagens dessas automações são tratadas como marketing: o canal de que o contato se descadastrou é pulado, e o e-mail leva o link de descadastro no rodapé. Para uma mensagem transacional, use o `send-message`. Veja [Descadastro de contatos](#descadastro-de-contatos).

## Erros

Erros que recusam a requisição inteira:

| HTTP | `error` | Causa |
| - | - | - |
| 400 | `communication_slug is required` | Envio único sem `communication_slug` |
| 400 | `messages array cannot be empty` | Lote com `messages` vazio |
| 400 | `Maximum 100 messages per batch request` | Lote com mais de 100 itens |
| 400 | `Each message must have communication_slug` | Algum item do lote sem `communication_slug` |
| 400 | `respect_opt_out must be a boolean` | Envio único com `respect_opt_out` que não é `true` nem `false` (ex.: `"true"` em texto) |
| 400 | `respect_opt_out must be a boolean in each message` | O mesmo, em algum item do lote |
| 401 | `Authorization header required. Use: Authorization: Bearer <token>` | Header `Authorization` ausente ou sem o prefixo `Bearer` |
| 401 | `Invalid, expired or revoked token` | Token vencido, client revogado ou credencial inválida. Peça um novo token |
| 404 | `Communication not found` | Envio único com slug inexistente ou comunicação inativa |
| 500 | mensagem do erro | Erro interno, inclusive corpo JSON malformado |
| 503 | `Não foi possível verificar o descadastro do contato. Nada foi enviado; tente de novo.` | Envio único com `respect_opt_out: true` em que a consulta ao descadastro falhou. Vem com `error_code: "opt_out_check_failed"`. Reenvie a mensagem |

Erros por canal, que chegam em `results[].error` com HTTP `200`:

| Mensagem | Status | Causa |
| - | - | - |
| `Canal não configurado` | `skipped` | O canal não está configurado na conta. Vem com `reason_code: "channel_not_configured"` e sem `log_id`: nada é registrado em **Logs** |
| `Limite diário de mensagens atingido` | `failed` | A conta atingiu o limite diário de mensagens. Vem com `error_code: "daily_limit_reached"` |
| `Canal in-app desabilitado` | `skipped` | A chave **Canal habilitado** do In-App está desligada |
| `Contato descadastrado` | `skipped` | Envio com `respect_opt_out: true` para um contato descadastrado do canal. Vem com `reason_code: "contato_descadastrado"` |
| `No template found for any language` | `skipped` | Nenhum template do canal casa com o idioma e há mais de um template ativo |
| `Destinatário sem os dados necessários para este canal — informe ...` | `skipped` | Canal pedido em `channels` sem o dado do destinatário |
| `Invalid phone number: ...` | `failed` | WhatsApp para número que não é celular brasileiro |
| `Número não está registrado no WhatsApp` | `skipped` | O número não tem WhatsApp |
| `No FCM token found — provide recipient.user_id or recipient.fcm_token` | `failed` | Nenhum navegador registrado para o `user_id` no Web Push |
| `No Telegram chat_id found — ...` ou `Nenhum usuário Telegram encontrado com o telefone ...` | `failed` | O usuário ainda não se vinculou ao bot do Telegram (ou não compartilhou o número com ele) |

## Exemplos por linguagem

```typescript Node.js theme={null}
const BASE_URL = 'https://api.octasend.com.br/functions/v1'

let cachedToken: { value: string; expiresAt: number } | null = null

async function getAccessToken(): Promise<string> {
  if (cachedToken && cachedToken.expiresAt > Date.now() + 60_000) {
    return cachedToken.value
  }

  const res = await fetch(`${BASE_URL}/oauth-token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'client_credentials',
      client_id: process.env.OCTASEND_CLIENT_ID,
      client_secret: process.env.OCTASEND_CLIENT_SECRET,
    }),
  })

  if (!res.ok) throw new Error(`Token error: ${await res.text()}`)

  const data = await res.json()
  cachedToken = {
    value: data.access_token,
    expiresAt: Date.now() + data.expires_in * 1000,
  }

  return cachedToken.value
}

async function sendMessage(slug: string, recipient: object, variables: object = {}, tag?: string | string[]) {
  const token = await getAccessToken()

  const res = await fetch(`${BASE_URL}/send-message`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      communication_slug: slug,
      recipient,
      variables,
      ...(tag ? { tag } : {}),
    }),
  })

  return res.json()
}

await sendMessage('boas-vindas', { email: 'usuario@exemplo.com' }, { nome: 'Ricardo' }, ['onboarding', 'junho-2024'])
await sendMessage('pedido-confirmado', { phone: '+5511999990000' }, { pedido: '12345' })
```

```python Python theme={null}
import os
import time
import requests

BASE_URL = 'https://api.octasend.com.br/functions/v1'

_token_cache = {'value': None, 'expires_at': 0}

def get_access_token() -> str:
    if _token_cache['value'] and _token_cache['expires_at'] > time.time() + 60:
        return _token_cache['value']

    res = requests.post(f'{BASE_URL}/oauth-token', json={
        'grant_type': 'client_credentials',
        'client_id': os.environ['OCTASEND_CLIENT_ID'],
        'client_secret': os.environ['OCTASEND_CLIENT_SECRET'],
    })
    res.raise_for_status()
    data = res.json()

    _token_cache['value'] = data['access_token']
    _token_cache['expires_at'] = time.time() + data['expires_in']

    return _token_cache['value']

def send_message(slug: str, recipient: dict, variables: dict = {}, tag = None) -> dict:
    token = get_access_token()

    payload = {
        'communication_slug': slug,
        'recipient': recipient,
        'variables': variables,
    }
    if tag is not None:
        payload['tag'] = tag

    res = requests.post(
        f'{BASE_URL}/send-message',
        headers={'Authorization': f'Bearer {token}'},
        json=payload,
    )
    res.raise_for_status()
    return res.json()

send_message('boas-vindas', {'email': 'usuario@exemplo.com'}, {'nome': 'Ricardo'}, ['onboarding', 'junho-2024'])
send_message('pedido-confirmado', {'phone': '+5511999990000'}, {'pedido': '12345'})
```

```php PHP theme={null}
<?php

define('BASE_URL', 'https://api.octasend.com.br/functions/v1');

$tokenCache = ['value' => null, 'expiresAt' => 0];

function getAccessToken(): string {
    global $tokenCache;

    if ($tokenCache['value'] && $tokenCache['expiresAt'] > time() + 60) {
        return $tokenCache['value'];
    }

    $ch = curl_init(BASE_URL . '/oauth-token');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode([
            'grant_type'    => 'client_credentials',
            'client_id'     => getenv('OCTASEND_CLIENT_ID'),
            'client_secret' => getenv('OCTASEND_CLIENT_SECRET'),
        ]),
    ]);

    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);

    $tokenCache['value']     = $data['access_token'];
    $tokenCache['expiresAt'] = time() + $data['expires_in'];

    return $tokenCache['value'];
}

function sendMessage(string $slug, array $recipient, array $variables = [], $tag = null): array {
    $token = getAccessToken();

    $payload = [
        'communication_slug' => $slug,
        'recipient'          => $recipient,
        'variables'          => $variables,
    ];
    if ($tag !== null) {
        $payload['tag'] = $tag;
    }

    $ch = curl_init(BASE_URL . '/send-message');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . $token,
        ],
        CURLOPT_POSTFIELDS => json_encode($payload),
    ]);

    $result = json_decode(curl_exec($ch), true);
    curl_close($ch);

    return $result;
}

sendMessage('boas-vindas', ['email' => 'usuario@exemplo.com'], ['nome' => 'Ricardo'], ['onboarding', 'junho-2024']);
sendMessage('pedido-confirmado', ['phone' => '+5511999990000'], ['pedido' => '12345']);
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.