> For the complete documentation index, see [llms.txt](https://docs.smsfunnel.com.br/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.smsfunnel.com.br/api/parceiros/webhook-de-status.md).

# Webhook de status

O **Webhook de status** entrega, na sua URL, o que aconteceu com cada mensagem — em vez de você consultar. Vale para todo disparo da conta: broadcast, automação, pela API ou pelo painel, envio real ou de teste.

{% hint style="info" %}
A URL de base de todos os endpoints é: <https://web.smsfunnel.com.br/api/parceiros/v1>
{% endhint %}

{% hint style="info" %}
O webhook **não substitui o polling como fonte da verdade**. O `status` do POST é o mesmo que `GET /sms/messages/{id}` devolve naquele instante; se os dois divergirem, a API é a fonte da verdade.
{% endhint %}

### São até dois eventos por mensagem

O primeiro evento sai no despacho e tem dois desfechos possíveis: ou a mensagem saiu, ou não saiu. O segundo sai quando a confirmação de entrega chega até nós — **quando e se** chegar.

| Evento                             | Quando                                                                                                                     | Garantido?                    |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| `sent_to_carrier`                  | No instante em que a mensagem é despachada da nossa fila para envio                                                        | Sim, se a mensagem saiu       |
| `cancelled` ou `blocked_blacklist` | Quando a mensagem **não** sai: telefone na sua blacklist, telefone inválido ou crédito insuficiente. O crédito é estornado | Sim, **no lugar** do primeiro |
| `delivered`                        | Quando a confirmação de entrega no aparelho chega até nós                                                                  | **Não**                       |

Quando a mensagem não sai, não haverá segundo evento — não há confirmação de entrega a receber.

**Não há prazo e não há desfecho garantido.** Nem todo envio produz confirmação de entrega; quando não produz, a mensagem fica em `sent_to_carrier` e não muda mais. Não trate a ausência do `delivered` como falha: `sent_to_carrier` é um estado possivelmente final. Use o `delivered` como confirmação positiva. Em geral, retornos de **delivered** chegam em até **2 horas** após o envio. Em alguns casos de telefone desligado ou retentativas o retorno pode demorar mais.

Isso é a mesma regra da consulta por API, e é deliberado: **não inventamos desfecho**. Um `delivered` que você recebe é uma confirmação real que chegou até nós.

**O cancelamento que parte de você não gera evento.** `PUT /sms/broadcasts/{id}/cancel` é um pedido seu, e devolver por POST o que você acabou de pedir não acrescenta informação. Os eventos de cancelamento existem para o que você **não** pediu.

### Quando você não recebe nada

O webhook começa a valer no momento do **envio**, não no da mudança de status. Uma mensagem só entra na fila de eventos se, quando ela saiu, a sua conta já tinha URL cadastrada.

| Situação                                          | O que você recebe                                                                                                       |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Cadastrou a URL **depois** de disparar            | Nada daquela mensagem — nem o `sent_to_carrier`, nem o `delivered` que chegar depois                                    |
| Removeu a URL e cadastrou de novo                 | Nada do que foi disparado no intervalo                                                                                  |
| Mensagem anterior à ativação do recurso na conta  | Nada                                                                                                                    |
| Mensagem anterior a 02/09/2026 criada fora da API | Nada — antes dessa data só o envio criado pela API recebia o `id` público que identifica a mensagem. Não houve backfill |

Em todos os casos o `status` continua correto em `GET /sms/messages/{id}` — só o POST não sai.

### Endpoints

#### PUT /me/webhook

**Função:** cadastra ou substitui a URL que recebe os eventos e o segredo usado para assinar.

**Corpo de envio:**

```json
{
    "url": "https://seu-dominio.com/webhooks/smsfunnel", // Obrigatório. HTTPS. Recusamos URL que resolva para endereço privado, loopback ou metadados de nuvem.
    "secret": "sua-chave-secreta-aqui" // Opcional. Mínimo 16 caracteres. Se omitido, geramos um.
}
```

**Corpo de resposta — 200 OK:**

```json
{
    "url": "https://seu-dominio.com/webhooks/smsfunnel",
    "secret": "whsec_aB3dE6gH9jK2mN5pQ8sT1vW4xY7zA0bC", // Só aparece AQUI. Guarde agora.
    "enabled": true
}
```

**Guarde o `secret` nesta resposta.** Ela é a única em que ele aparece — o `GET` devolve apenas `has_secret`. Se perder, faça um novo `PUT`: o segredo anterior é substituído.

Com o header `X-On-Behalf-Of`, a configuração é gravada na conta filha, que é de quem são as mensagens.

#### GET /me/webhook

**Função:** mostra a configuração atual da conta. O segredo nunca é devolvido.

**Corpo de resposta — 200 OK:**

```json
{
    "url": "https://seu-dominio.com/webhooks/smsfunnel", // Nulo se não houver webhook cadastrado
    "has_secret": true, // Se existe segredo gravado
    "enabled": true // Se a conta está recebendo eventos
}
```

#### DELETE /me/webhook

**Função:** desliga o envio de eventos e apaga URL e segredo.

Responde **200 OK**. A partir daí nenhuma mensagem nova entra na fila de eventos — inclusive as disparadas antes de você cadastrar de novo.

### O evento

Um único tipo de evento (`message.status`) para os dois POSTs. O formato não muda; o que muda é o `status`. Você implementa um handler só.

```json
{
    "event": "message.status", // Sempre "message.status"
    "id": "0192a2c5-7d92-7c4a-9b1c-1a2b3c4d5e6f", // O mesmo id de GET /sms/messages/{id}
    "reference": "order-123", // Sua referência. NULO em automação — o campo só existe em broadcast, aqui e na API.
    "phone": "5511999999999", // Telefone de destino
    "status": "delivered", // sent_to_carrier | delivered | cancelled | blocked_blacklist
    "sent_at": "2026-08-03T14:25:13-03:00", // Instante do despacho. NULO quando a mensagem não saiu.
    "origin": { // De onde a mensagem saiu
        "type": "broadcast", // "broadcast" ou "automation"
        "id": "0192a2c4-7d92-7c4a-9b1c-1a2b3c4d5e6f", // Id do broadcast, ou da sequência em automação
        "name": "Black Friday 2026" // Nome do broadcast, ou da campanha em automação
    }
}
```

Os campos de mensagem são exatamente os de `GET /sms/messages/{id}`. Não há campo exclusivo do webhook além de `origin`.

#### O bloco origin

É o que distingue um disparo em massa de uma régua de automação sem você cruzar o `id` contra as suas tabelas. Em automação ele ganha um campo a mais:

```json
{
    "origin": {
        "type": "automation",
        "id": "0192d310-7d92-7c4a-9b1c-1a2b3c4d5e6f", // Id da SEQUÊNCIA — a mensagem específica da régua
        "name": "Recuperação de carrinho", // Nome da CAMPANHA
        "campaign_id": "0192d300-7d92-7c4a-9b1c-1a2b3c4d5e6f" // Só em automação
    }
}
```

Em automação o nome vem da campanha porque a sequência não tem nome próprio — ela é uma posição na régua. O `campaign_id` acompanha para você conseguir agrupar.

### Verificando a assinatura

Todo POST leva dois headers:

| Header                  | Conteúdo                                                               |
| ----------------------- | ---------------------------------------------------------------------- |
| `X-SmsFunnel-Signature` | `sha256=` seguido do HMAC-SHA256 do **corpo bruto**, com o seu segredo |
| `X-SmsFunnel-Event-Id`  | Identificador do evento, para idempotência                             |

Assine o corpo **exatamente como recebido**, antes de qualquer parse — reserializar o JSON muda os bytes e quebra a comparação.

```php
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $seuSegredo);

if (! hash_equals($expected, $request->header('X-SmsFunnel-Signature'))) {
    abort(401);
}
```

**Use o `X-SmsFunnel-Event-Id` para idempotência.** As retentativas de um mesmo evento repetem o id: se você já processou aquele id, descarte.

### Entrega e retentativas

| Aspecto     | Comportamento                                                                                |
| ----------- | -------------------------------------------------------------------------------------------- |
| Timeout     | 5 segundos por tentativa                                                                     |
| Sucesso     | Qualquer 2xx                                                                                 |
| Retentativa | 5xx, 408, 429 e falha de rede — até 5 tentativas, com espera de 10s, 30s, 1min, 5min e 15min |
| Desistência | Demais 4xx — tratamos como endereço ou contrato errado, e não insistimos                     |

Responda rápido: processe de forma assíncrona e devolva 200 imediatamente. Um endpoint lento gera retentativa mesmo tendo recebido o evento.
