> ## 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

> Receive order, tracking and return-receipt events on your own endpoint, signed and retried.

Webhooks push events to your server as they happen, so you don't have to poll
`GET /api/v2/get`. You register an endpoint, pick the events you care about, and
Escrybe `POST`s a JSON payload to it.

Endpoints are managed in the panel under **Configurações → Webhooks**
(`/opcoes/webhooks`), where you can also see every delivery attempt, its response
and retry a failed one.

## Events

| Event                  | Fires when                                                                |
| ---------------------- | ------------------------------------------------------------------------- |
| `order.created`        | A letter or registered e-mail order is created                            |
| `order.status_updated` | An order changes status                                                   |
| `tracking.updated`     | A tracking code is assigned, or Correios reports new tracking events      |
| `rr.created`           | A return receipt (AR/RR) becomes available — the payload carries the file |
| `webhook.test`         | Only when you press **Testar** in the panel                               |

## Envelope

Every delivery has the same top-level shape:

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

| Field         | Description                                                      |
| ------------- | ---------------------------------------------------------------- |
| `event`       | Event name from the table above                                  |
| `environment` | `production` or `homolog` — see below                            |
| `timestamp`   | When the event was generated, `Y-m-d H:i:s` (America/Sao\_Paulo) |
| `webhook_id`  | The endpoint registration that matched                           |
| `delivery_id` | Unique id for this delivery attempt — use it to deduplicate      |
| `data`        | Event-specific body                                              |

<Note>
  `data` is additive: new fields may appear over time. Read the fields you need
  and ignore the rest rather than validating against a closed schema.
</Note>

## Environment

`environment` tells you which Escrybe environment produced the event:

| Value        | Meaning                                        |
| ------------ | ---------------------------------------------- |
| `production` | A real order on `app.escrybe.com.br`           |
| `homolog`    | The test environment, `homolog.escrybe.com.br` |

It is also sent as the `X-Webhook-Environment` header, so you can route or drop
an event before parsing the body.

<Tip>
  If you point both environments at the same endpoint, branch on this field.
  Homolog orders can be advanced through their whole lifecycle by hand, so they
  produce the same events as production — `environment` is what tells them apart.
</Tip>

## Headers

| Header                  | Value                                                |
| ----------------------- | ---------------------------------------------------- |
| `Content-Type`          | `application/json`                                   |
| `User-Agent`            | `Escrybe-Webhooks/1.0`                               |
| `X-Webhook-Event`       | Event name                                           |
| `X-Webhook-Delivery`    | Delivery id (same as `delivery_id`)                  |
| `X-Webhook-Timestamp`   | Attempt time, `Y-m-d H:i:s`                          |
| `X-Webhook-Environment` | `production` or `homolog`                            |
| `X-Webhook-Signature`   | `sha256=<hex>` — only when the endpoint has a secret |

## Verifying the signature

When you set a secret, each delivery is signed with HMAC-SHA256 over the **raw
request body**. Compare with a constant-time function, and read the body before
any JSON parsing or middleware rewrites it.

<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('Invalid signature');
  }

  $data = json_decode($payload, true);

  if (($data['environment'] ?? '') !== 'production') {
      http_response_code(200); // test event — acknowledge, don't process
      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;

  // Raw body is required — express.json() would reparse and break the signature
  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('Invalid signature');
    }

    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 "Invalid signature", 401

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

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

## Delivery and retries

* Your endpoint must answer **2xx** within **30 seconds**.
* A failed delivery is retried up to **3 times** with exponential backoff:
  **5, 10 and 20 minutes**.
* After **3 consecutive failures** on real events the endpoint is **paused
  automatically** and you get an e-mail. Resume it in the panel.
* The response body is stored (up to 10,000 characters) so you can see what your
  server answered.
* Manual test deliveries are never retried and never count toward the pause.

<Note>
  Deliveries are at-least-once. A retry can land after your server already
  processed the event — for instance when it answered late. Key your processing
  on `delivery_id`, or make the handler idempotent per order id and status.
</Note>

## Payloads

### order.created

Letter, telegram and e-Carta orders:

```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"
    }
  }
}
```

Registered e-mail orders carry `type: "email"` and a different `data`:

```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" }
    ]
  }
}
```

For `type: "email"` the body carries `opened` and `date_opened` instead of
`tracking` and `files`.

Status values for letters, telegrams and e-Carta:

| `status` | Meaning              |
| -------- | -------------------- |
| 0        | Processing           |
| 1        | In production        |
| 2        | Insufficient credits |
| 3        | Printed              |
| 4        | Posted               |
| 5        | Cancelled            |
| 6        | Returned             |

Status values for registered e-mail:

| `status` | Meaning              |
| -------- | -------------------- |
| 0        | Processing payment   |
| 1        | Paid, awaiting send  |
| 2        | Insufficient credits |
| 3        | Sent                 |
| 4        | Cancelled            |
| 5        | Failed               |

### 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` is `new_tracking_code` when the code is first assigned and
`new_tracking_record` when Correios reports new events. `tracking_records` is the
Correios object as returned by them, newest event first. `status_delivery` is
empty, `delivered`, `going_back_to_sender` or `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` is `rr` for a scanned physical receipt and `rr_electronic` for the one
Correios returns digitally. `file` is `null` when the document could not be read.

<Note>
  This payload embeds the whole document in base64 and is much larger than the
  others. Make sure your endpoint accepts a body of a few 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"
  }
}
```

## Testing against homolog

`homolog.escrybe.com.br` is a full copy of the platform with its own database
and credentials. Orders there let you exercise every event without posting
anything real: on the orders page you can move an order through its statuses by
hand and add tracking events, and each one fires the same webhook production
would, tagged `environment: "homolog"`.

<Note>
  Registered e-mail and WhatsApp are delivered for real from homolog, so
  recipients must first be registered in **Whitelist de homologação**
  (`/homolog-whitelist`). Anything not on the list is refused and the order is
  marked as failed with the reason.
</Note>

Ask support for homolog access.
