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

# Webhooks

> Receba no seu backend os eventos da Arara: receptor mínimo, verificação de assinatura, retentativas e teste local.

A Arara faz um `POST` na URL do seu backend sempre que algo acontece: mensagem entregue, cliente respondeu, template aprovado, cobrança paga.

## Receba seu primeiro evento

Você precisa de Node 18 ou mais novo e de acesso ao [painel](https://app.ararahq.com/dashboard/webhooks).

<Steps>
  <Step title="Crie o receptor">
    Salve como `receptor.mjs`. Ele lê o corpo bruto, confere a assinatura, responde `200` na hora e registra o evento.

    ```javascript receptor.mjs theme={null}
    import { createServer } from 'node:http';
    import { createHmac, timingSafeEqual } from 'node:crypto';

    const PORT = 3000;
    const TOLERANCE_SECONDS = 300;
    const secret = process.env.ARARA_WEBHOOK_SECRET;

    if (!secret) {
      console.error('Defina ARARA_WEBHOOK_SECRET antes de iniciar.');
      process.exit(1);
    }

    function isValidSignature(rawBody, timestamp, signature) {
      if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
      if (!/^\d+$/.test(timestamp) || !signature.startsWith('sha256=')) return false;
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;

      const expected = createHmac('sha256', secret)
        .update(`${timestamp}.`)
        .update(rawBody)
        .digest('hex');
      const a = Buffer.from(expected);
      const b = Buffer.from(signature.slice('sha256='.length));
      return a.length === b.length && timingSafeEqual(a, b);
    }

    const server = createServer((req, res) => {
      if (req.method !== 'POST' || req.url !== '/webhooks/arara') {
        res.writeHead(404).end();
        return;
      }

      const chunks = [];
      req.on('data', (chunk) => chunks.push(chunk));
      req.on('end', () => {
        const rawBody = Buffer.concat(chunks);
        const timestamp = req.headers['x-arara-timestamp'];
        const signature = req.headers['x-arara-signature'];

        if (!isValidSignature(rawBody, timestamp, signature)) {
          res.writeHead(401).end();
          return;
        }

        res.writeHead(200).end();

        const payload = JSON.parse(rawBody.toString('utf8'));
        console.log(
          req.headers['x-arara-delivery-id'],
          payload.event,
          payload.data?.id ?? '-',
          payload.data?.status ?? '-',
        );
      });
    });

    server.listen(PORT, () => {
      console.log(`Ouvindo em http://localhost:${PORT}/webhooks/arara`);
    });
    ```
  </Step>

  <Step title="Exponha a porta local">
    A Arara precisa de um endereço `https` público. Em outro terminal, abra um túnel para a porta `3000`:

    ```bash theme={null}
    cloudflared tunnel --url http://localhost:3000
    ```

    Copie o endereço `https://...` que o túnel mostrar.
  </Step>

  <Step title="Cadastre a URL e copie o secret">
    No painel, abra [Webhooks](https://app.ararahq.com/dashboard/webhooks):

    1. Em **URL**, cole o endereço do túnel seguido de `/webhooks/arara`.
    2. Gere o **secret** e copie o valor. Ele começa com `whsec_`.

    Faça isso antes de testar. Sem o secret, o receptor recusa todos os eventos.
  </Step>

  <Step title="Rode o receptor">
    ```bash theme={null}
    ARARA_WEBHOOK_SECRET=whsec_... node receptor.mjs
    ```
  </Step>

  <Step title="Dispare o evento de teste">
    Na mesma tela do painel, dispare o evento de teste. Em alguns segundos o terminal mostra:

    ```
    5d2f7c1a-8b3e-4f6d-9a0b-1c2d3e4f5a6b webhook.test - -
    ```

    O `webhook.test` não tem `data.id` nem `data.status`. Envie uma mensagem e você verá linhas como `message.status_updated ara_msg_k9Bf2Xq delivered`.
  </Step>
</Steps>

<Note>
  A URL é uma por organização e só se configura pelo painel. Chave de API não dá acesso a essa configuração. O histórico de entregas, o reenvio e o evento de teste também ficam só no painel.
</Note>

A lista de eventos e o formato do corpo estão em [Eventos](/webhooks/events).

## Verificar a assinatura

1. Leia o corpo bruto, os bytes como chegaram, antes de qualquer parse.
2. Calcule `HMAC-SHA256(secret, "{X-Arara-Timestamp}.{corpo bruto}")` em hexadecimal.
3. Compare, em tempo constante, com o valor de `X-Arara-Signature` depois de `sha256=`. Recuse se o timestamp estiver a mais de 5 minutos do seu relógio.

<CodeGroup>
  ```javascript Node.js theme={null}
  import { createHmac, timingSafeEqual } from 'node:crypto';

  const TOLERANCE_SECONDS = 300;

  export function isValidArara(rawBody, headers, secret) {
    const timestamp = headers['x-arara-timestamp'];
    const signature = headers['x-arara-signature'];
    if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
    if (!/^\d+$/.test(timestamp) || !signature.startsWith('sha256=')) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;

    const expected = createHmac('sha256', secret)
      .update(`${timestamp}.`)
      .update(rawBody)
      .digest('hex');
    const a = Buffer.from(expected);
    const b = Buffer.from(signature.slice('sha256='.length));
    return a.length === b.length && timingSafeEqual(a, b);
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import time

  TOLERANCE_SECONDS = 300

  def is_valid_arara(raw_body: bytes, headers: dict, secret: str) -> bool:
      timestamp = headers.get("X-Arara-Timestamp")
      signature = headers.get("X-Arara-Signature")
      if not isinstance(timestamp, str) or not isinstance(signature, str):
          return False
      if not timestamp.isdigit() or not signature.startswith("sha256="):
          return False
      if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
          return False
      expected = hmac.new(
          secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature[len("sha256="):])
  ```

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

  const TOLERANCE_SECONDS = 300;

  function isValidArara(string $rawBody, array $headers, string $secret): bool
  {
      $timestamp = $headers['X-Arara-Timestamp'] ?? '';
      $signature = $headers['X-Arara-Signature'] ?? '';
      if (!is_string($timestamp) || !is_string($signature)) {
          return false;
      }
      if (!ctype_digit($timestamp) || !str_starts_with($signature, 'sha256=')) {
          return false;
      }
      if (abs(time() - (int) $timestamp) > TOLERANCE_SECONDS) {
          return false;
      }
      $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
      return hash_equals($expected, substr($signature, strlen('sha256=')));
  }
  ```
</CodeGroup>

<Warning>
  Compare sempre em tempo constante (`timingSafeEqual`, `hmac.compare_digest`, `hash_equals`). Com `==`, dá para descobrir a assinatura byte a byte medindo o tempo de resposta. Verifique a assinatura antes de gravar qualquer coisa.
</Warning>

Se você não definir um secret, a Arara gera um na primeira entrega. O painel mostra o secret atual.

## Entrega

Responda com qualquer status `2xx` em até 5 segundos. Todo o resto conta como falha: outro status (inclusive `3xx`, que não é seguido), timeout ou erro de conexão.

Cada entrega tem até 5 tentativas:

| Tentativa | Quando |
| - | - |
| 1ª | Na hora do evento |
| 2ª | 30 segundos depois da 1ª falha |
| 3ª | 2 minutos depois da 2ª falha |
| 4ª | 15 minutos depois da 3ª falha |
| 5ª | 1 hora depois da 4ª falha |

Depois da 5ª falha a entrega é marcada como falha definitiva e a organização recebe um alerta no painel. Você ainda pode reenviá-la pelo histórico de entregas.

### Desativação automática

A Arara desativa o webhook quando as duas condições valem ao mesmo tempo: 50 falhas seguidas e 30 minutos falhando sem nenhuma entrega bem-sucedida. Uma entrega com sucesso zera a contagem.

<Warning>
  Com o webhook desativado, os eventos novos não são guardados nem reenviados depois. Corrija o endpoint e reative pelo painel. Para saber o status de uma mensagem que você perdeu, consulte `GET /v1/messages/{id}`.
</Warning>

### Deduplicação

Todas as tentativas e reenvios de uma entrega repetem o mesmo `X-Arara-Delivery-Id`. Grave esse valor com restrição de unicidade antes de processar e ignore os repetidos.

```sql theme={null}
INSERT INTO arara_webhook_deliveries (delivery_id, event, received_at)
VALUES ('5d2f7c1a-8b3e-4f6d-9a0b-1c2d3e4f5a6b', 'message.status_updated', NOW())
ON CONFLICT (delivery_id) DO NOTHING
RETURNING delivery_id;
```

Sem linha retornada, é repetido: responda `200` e não processe.

O mesmo fato também pode chegar em entregas diferentes, por exemplo dois `message.status_updated` com o mesmo `status`. Deixe o handler idempotente também pela chave de negócio, como `data.id` + `data.status`.

### Ordem

A ordem não é garantida. Um `delivered` pode chegar depois do `read` da mesma mensagem. Compare com o estado que você já tem e nunca regrida um status.

### Processe em fila

Responda `200` assim que a assinatura for válida e o `X-Arara-Delivery-Id` estiver gravado. O trabalho demorado (chamar outra API, enviar e-mail, atualizar pedido) vai para uma fila sua. Processar dentro da requisição estoura os 5 segundos e gera retentativa.

## Testar localmente

### Com a CLI

O `arara listen` da [CLI](/cli) recebe os eventos da organização no terminal e encaminha cada um para o seu receptor local, sem túnel.

<Steps>
  <Step title="Autentique a CLI">
    ```bash theme={null}
    arara login
    ```
  </Step>

  <Step title="Veja os eventos chegando">
    ```bash theme={null}
    arara listen
    ```

    Para filtrar por tipo:

    ```bash theme={null}
    arara listen --events message.status_updated,message.received
    ```
  </Step>

  <Step title="Encaminhe para o seu receptor">
    Com o `receptor.mjs` rodando na porta `3000`:

    ```bash theme={null}
    arara listen --forward-to http://localhost:3000/webhooks/arara
    ```

    Para receber o evento assinado, com o header `X-Arara-Signature`, use `--capture`:

    ```bash theme={null}
    arara listen --capture --forward-to http://localhost:3000/webhooks/arara
    ```
  </Step>
</Steps>

<Warning>
  Enquanto um `arara listen --capture` está ativo, os eventos da organização vão para a CLI em vez da URL cadastrada no painel. Não deixe a captura ligada numa organização em produção.
</Warning>

Com `--capture`, a assinatura é feita com o secret do listener temporário da CLI, não com o secret do painel. Para validar a verificação de assinatura de ponta a ponta com o secret de produção, use o túnel.

### Com túnel

Qualquer túnel que entregue um endereço `https` público serve:

```bash theme={null}
cloudflared tunnel --url http://localhost:3000
# ou
ngrok http 3000
```

Cadastre o endereço gerado no painel, seguido do caminho do seu receptor, e dispare o evento de teste. O painel mostra, para cada entrega, o corpo enviado, os headers e a resposta do seu servidor.

## Headers enviados

| Header | Exemplo | Descrição |
| - | - | - |
| `Content-Type` | `application/json` | O corpo é sempre JSON. |
| `User-Agent` | `Arara-Webhook/1.0` | Identifica a Arara. |
| `X-Arara-Event` | `message.status_updated` | Tipo do evento. Mesmo valor de `event` no corpo. |
| `X-Arara-Delivery-Id` | `5d2f7c1a-8b3e-4f6d-9a0b-1c2d3e4f5a6b` | UUID da entrega. Igual em todas as tentativas e reenvios. |
| `X-Arara-Api-Version` | `2026-05-13` | Versão do contrato dos payloads. Mesmo valor de `apiVersion` no corpo. |
| `X-Arara-Timestamp` | `1789482731` | Epoch em segundos do momento da tentativa. Muda a cada tentativa. |
| `X-Arara-Signature` | `sha256=a4f9b8c2...` | `sha256=` seguido do HMAC-SHA256 em hexadecimal. |

## Próximos passos

<CardGroup cols={2}>
  <Card title="Eventos" icon="list-check" href="/webhooks/events">
    Corpo e campos de cada evento.
  </Card>

  <Card title="Next.js" icon="code" href="/integracoes/nextjs#webhook">
    Route handler com verificação de assinatura.
  </Card>

  <Card title="Laravel" icon="php" href="/integracoes/laravel#webhook">
    Controller com verificação de assinatura.
  </Card>

  <Card title="Por que minha mensagem falhou" icon="triangle-exclamation" href="/erros-de-envio">
    O que cada motivo de falha significa.
  </Card>
</CardGroup>


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