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

# Webhooks

> Receba eventos de pedidos, rastreio e AR no seu próprio endpoint, assinados e com retentativa.

Webhooks empurram os eventos para o seu servidor assim que acontecem, evitando
ficar consultando `GET /api/v2/get`. Você cadastra um endpoint, escolhe os
eventos que interessam e a Escrybe faz um `POST` com um payload JSON nele.

Os endpoints são gerenciados no painel em **Configurações → Webhooks**
(`/opcoes/webhooks`), onde você também vê cada tentativa de entrega, a resposta
que seu servidor deu e pode reenviar uma que falhou.

## Eventos

| Evento                 | Quando dispara                                                             |
| ---------------------- | -------------------------------------------------------------------------- |
| `order.created`        | Um pedido de carta ou e-mail registrado é criado                           |
| `order.status_updated` | Um pedido muda de status                                                   |
| `tracking.updated`     | Um código de rastreio é atribuído, ou os Correios registram novos eventos  |
| `rr.created`           | Um Aviso de Recebimento (AR/RR) fica disponível — o payload traz o arquivo |
| `webhook.test`         | Somente quando você clica em **Testar** no painel                          |

## Envelope

Toda entrega tem o mesmo formato no nível de cima:

```json theme={null}
{
  "event": "order.created",
  "environment": "production",
  "timestamp": "2026-08-18 10:30:00",
  "webhook_id": 123,
  "delivery_id": 456,
  "data": { }
}
```

| Campo         | Descrição                                                      |
| ------------- | -------------------------------------------------------------- |
| `event`       | Nome do evento, conforme a tabela acima                        |
| `environment` | `production` ou `homolog` — veja abaixo                        |
| `timestamp`   | Quando o evento foi gerado, `Y-m-d H:i:s` (America/Sao\_Paulo) |
| `webhook_id`  | Cadastro de endpoint que casou com o evento                    |
| `delivery_id` | Id único desta tentativa de entrega — use para deduplicar      |
| `data`        | Corpo específico do evento                                     |

<Note>
  `data` é aditivo: campos novos podem aparecer com o tempo. Leia os campos que
  você usa e ignore o resto, em vez de validar contra um schema fechado.
</Note>

## Ambiente

`environment` diz qual ambiente da Escrybe produziu o evento:

| Valor        | Significado                                  |
| ------------ | -------------------------------------------- |
| `production` | Pedido real, em `app.escrybe.com.br`         |
| `homolog`    | Ambiente de testes, `homolog.escrybe.com.br` |

O mesmo valor vai no header `X-Webhook-Environment`, então dá para desviar ou
descartar um evento antes de fazer o parse do corpo.

<Tip>
  Se você aponta os dois ambientes para o mesmo endpoint, use esse campo para
  separá-los. Pedidos de homologação podem ser levados por todo o ciclo de vida
  na mão, então produzem os mesmos eventos que a produção — é o `environment`
  que os distingue.
</Tip>

## Headers

| Header                  | Valor                                             |
| ----------------------- | ------------------------------------------------- |
| `Content-Type`          | `application/json`                                |
| `User-Agent`            | `Escrybe-Webhooks/1.0`                            |
| `X-Webhook-Event`       | Nome do evento                                    |
| `X-Webhook-Delivery`    | Id da entrega (o mesmo do `delivery_id`)          |
| `X-Webhook-Timestamp`   | Hora da tentativa, `Y-m-d H:i:s`                  |
| `X-Webhook-Environment` | `production` ou `homolog`                         |
| `X-Webhook-Signature`   | `sha256=<hex>` — só quando o endpoint tem segredo |

## Verificando a assinatura

Quando você define um segredo, cada entrega é assinada com HMAC-SHA256 sobre o
**corpo bruto da requisição**. Compare com uma função de tempo constante e leia
o corpo antes de qualquer parse de JSON ou middleware que o reescreva.

<CodeGroup>
  ```php PHP theme={null}
  $secret    = getenv('ESCRYBE_WEBHOOK_SECRET');
  $payload   = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

  $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);

  if (!hash_equals($expected, $signature)) {
      http_response_code(401);
      exit('Assinatura inválida');
  }

  $data = json_decode($payload, true);

  if (($data['environment'] ?? '') !== 'production') {
      http_response_code(200); // evento de teste — confirme, mas não processe
      exit;
  }

  http_response_code(200);
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');
  const express = require('express');

  const app = express();
  const SECRET = process.env.ESCRYBE_WEBHOOK_SECRET;

  // O corpo bruto é obrigatório — express.json() reserializa e quebra a assinatura
  app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.header('X-Webhook-Signature') || '';
    const expected = 'sha256=' + crypto.createHmac('sha256', SECRET)
                                       .update(req.body)
                                       .digest('hex');

    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send('Assinatura inválida');
    }

    const data = JSON.parse(req.body.toString());
    if (data.environment !== 'production') return res.sendStatus(200);

    res.sendStatus(200);
  });
  ```

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

  from flask import Flask, request

  app = Flask(__name__)
  SECRET = os.environ["ESCRYBE_WEBHOOK_SECRET"].encode()

  @app.post("/webhook")
  def webhook():
      payload = request.get_data()
      signature = request.headers.get("X-Webhook-Signature", "")
      expected = "sha256=" + hmac.new(SECRET, payload, hashlib.sha256).hexdigest()

      if not hmac.compare_digest(expected, signature):
          return "Assinatura inválida", 401

      data = request.get_json()
      if data.get("environment") != "production":
          return "", 200

      return "", 200
  ```
</CodeGroup>

## Entrega e retentativas

* Seu endpoint precisa responder **2xx** em até **30 segundos**.
* Uma entrega que falha é tentada de novo até **3 vezes**, com backoff
  exponencial: **5, 10 e 20 minutos**.
* Após **3 falhas consecutivas** em eventos reais o endpoint é **pausado
  automaticamente** e você recebe um e-mail. Reative no painel.
* O corpo da resposta é armazenado (até 10.000 caracteres) para você ver o que
  seu servidor respondeu.
* Entregas de teste nunca são repetidas e nunca contam para a pausa.

<Note>
  A entrega é *at-least-once*. Uma retentativa pode chegar depois do seu servidor
  já ter processado o evento — por exemplo quando ele respondeu com atraso. Use
  o `delivery_id` como chave, ou faça o handler idempotente por id do pedido e
  status.
</Note>

## Payloads

### order.created

Pedidos de carta, telegrama e e-Carta:

```json theme={null}
{
  "event": "order.created",
  "environment": "production",
  "timestamp": "2026-08-18 10:30:00",
  "webhook_id": 123,
  "delivery_id": 456,
  "data": {
    "id": "260818001",
    "type": "letter",
    "status": 0,
    "shipType": "2",
    "numPages": 3,
    "value": 15.50,
    "created_at": "2026-08-18 10:30:00",
    "paymentMethod": "invoice",
    "recipient": {
      "name": "João Silva",
      "addr1": "Rua das Flores, 123",
      "addr2": "Apto 45",
      "city": "São Paulo",
      "state": "SP",
      "zip": "01234567",
      "country": "Brasil"
    },
    "sender": {
      "name_sender": "Minha Empresa LTDA",
      "addr1_sender": "Av. Paulista, 1000",
      "addr2_sender": "Sala 12",
      "city_sender": "São Paulo",
      "state_sender": "SP",
      "zip_sender": "01310100",
      "country_sender": "Brasil"
    }
  }
}
```

Pedidos de e-mail registrado trazem `type: "email"` e um `data` diferente:

```json theme={null}
{
  "id": "260818002",
  "type": "email",
  "status": 0,
  "recipient": { "name": "João Silva", "email": "joao@exemplo.com.br" },
  "sender": {
    "name_sender": "Minha Empresa LTDA",
    "email_sender": "contato@escrybe.com.br",
    "email_replyTo": "financeiro@minhaempresa.com.br"
  },
  "subject": "Notificação de cobrança",
  "value": 4.90,
  "created_at": "2026-08-18 10:30:00",
  "paymentMethod": "credits"
}
```

### order.status\_updated

```json theme={null}
{
  "event": "order.status_updated",
  "environment": "production",
  "timestamp": "2026-08-18 14:02:11",
  "webhook_id": 123,
  "delivery_id": 457,
  "data": {
    "id": "260818001",
    "type": "letter",
    "status": { "old": 1, "new": 3 },
    "updated_at": "2026-08-18 14:02:11",
    "tracking": "RB123456789BR",
    "files": [
      { "type": "pdf", "created_at": "2026-08-18 10:30:00" }
    ]
  }
}
```

Para `type: "email"` o corpo traz `opened` e `date_opened` no lugar de
`tracking` e `files`.

Status de carta, telegrama e e-Carta:

| `status` | Significado            |
| -------- | ---------------------- |
| 0        | Processando            |
| 1        | Em produção            |
| 2        | Créditos insuficientes |
| 3        | Impresso               |
| 4        | Postado                |
| 5        | Cancelado              |
| 6        | Devolvido              |

Status de e-mail registrado:

| `status` | Significado            |
| -------- | ---------------------- |
| 0        | Processando pagamento  |
| 1        | Pago, aguardando envio |
| 2        | Créditos insuficientes |
| 3        | Enviado                |
| 4        | Cancelado              |
| 5        | Falha                  |

### tracking.updated

```json theme={null}
{
  "event": "tracking.updated",
  "environment": "production",
  "event_detail_type": "new_tracking_record",
  "timestamp": "2026-08-19 09:15:00",
  "webhook_id": 123,
  "delivery_id": 458,
  "data": {
    "id": "260818001",
    "type": "letter",
    "tracking_code": "RB123456789BR",
    "tracking_records": {
      "codObjeto": "RB123456789BR",
      "eventos": [
        {
          "codigo": "BDE",
          "dtHrCriado": "2026-08-19T09:12:00",
          "descricao": "Objeto entregue ao destinatário",
          "unidade": { "endereco": { "cidade": "São Paulo", "uf": "SP" } }
        }
      ]
    },
    "status_delivery": "delivered",
    "updated_at": "2026-08-19 09:15:00"
  }
}
```

`event_detail_type` é `new_tracking_code` quando o código é atribuído pela
primeira vez e `new_tracking_record` quando os Correios registram novos eventos.
`tracking_records` é o objeto dos Correios como eles devolvem, do evento mais
novo para o mais antigo. `status_delivery` é vazio, `delivered`,
`going_back_to_sender` ou `delivered_to_sender`.

### rr.created

```json theme={null}
{
  "event": "rr.created",
  "environment": "production",
  "timestamp": "2026-08-25 11:40:00",
  "webhook_id": 123,
  "delivery_id": 459,
  "data": {
    "id": "260818001",
    "type": "letter",
    "rr_type": "rr_electronic",
    "tracking_code": "RB123456789BR",
    "file": {
      "name": "260818001_rr.pdf",
      "base64": "JVBERi0xLjQKJ..."
    }
  }
}
```

`rr_type` é `rr` para o AR físico digitalizado e `rr_electronic` para o que os
Correios devolvem digitalmente. `file` vem `null` quando o documento não pôde
ser lido.

<Note>
  Este payload embute o documento inteiro em base64 e é bem maior que os outros.
  Garanta que seu endpoint aceita um corpo de alguns megabytes.
</Note>

### webhook.test

```json theme={null}
{
  "event": "webhook.test",
  "environment": "production",
  "timestamp": "2026-08-18 10:00:00",
  "webhook_id": 123,
  "delivery_id": 460,
  "data": {
    "message": "Esta é uma entrega de teste iniciada pelo usuário.",
    "webhook_id": 123,
    "webhook_name": "Meu endpoint"
  }
}
```

## Testando no homolog

`homolog.escrybe.com.br` é uma cópia completa da plataforma, com banco e
credenciais próprios. Os pedidos de lá permitem exercitar todos os eventos sem
postar nada de verdade: na página de pedidos você move o pedido pelos status na
mão e adiciona eventos de rastreio, e cada ação dispara o mesmo webhook que a
produção dispararia, marcado com `environment: "homolog"`.

<Note>
  E-mail registrado e WhatsApp são entregues de verdade a partir do homolog, então
  os destinatários precisam estar cadastrados em **Whitelist de homologação**
  (`/homolog-whitelist`). Quem não estiver na lista tem o envio recusado e o
  pedido fica com status de falha, explicando o motivo.
</Note>

Peça acesso ao homolog para o suporte.
