# Webhooks

> **Section:** [Introduction](https://docs.editsquare.com/api.md)
> **Related:** [Authentication](https://docs.editsquare.com/api/authentication.md) · [Errors](https://docs.editsquare.com/api/errors.md) · [Pagination](https://docs.editsquare.com/api/pagination.md) · [Renders](https://docs.editsquare.com/api/renders.md)
> **Also:** [HTML version](https://docs.editsquare.com/api/webhooks) · [Docs index](https://docs.editsquare.com/llms.txt)

---
Listen for events on your webhook endpoint so your integration can react as
renders progress, without polling.

After you register a webhook endpoint, Edit Square `POST`s a JSON payload to
it when a render changes state. Receiving webhook events is the right way to
respond to asynchronous work such as a render completing or failing,
especially at volume.

## Set up your endpoint

Webhook endpoints are configured per team in the dashboard, under
**Webhooks**:

1. Register your endpoint's publicly accessible HTTPS URL.
2. Select the events you want delivered to it.
3. Copy the signing secret generated for the endpoint. You will use it to
   verify deliveries.

## Events

| Event | Sent when |
| --- | --- |
| `render.status_changed` | A render's status changes, including both `complete` and `failed`. |
| `render.complete` | A render finished successfully. |
| `render.failed` | A render stopped with an error. |

Only subscribe to the events your integration requires. An endpoint
subscribed to both `render.status_changed` and `render.complete` receives
**two** deliveries when a render completes, one for each event.

## The event payload

```json
{
  "id": "hook_jn8x…",
  "event": "render.complete",
  "data": {
    "id": "rend_jh716ctpaqbye9aw64tgkqwtks7gvx7g",
    "name": "Example render name",
    "status": "complete",
    "project": { "id": "proj_jd71dcg…", "team": "team_jn7fw41…" },
    "output": { "master": "https://…" }
  },
  "timestamp": 1748875986000
}
```

`data` is the render, in the same shape
[`GET /v1/renders/{id}`](/api/reference/operations/getrender/) returns.

Every delivery also carries these headers:

| Header | Value |
| --- | --- |
| `X-Webhook-Signature` | `sha256=<hex>`, an HMAC-SHA256 of the raw body, keyed with the endpoint's secret. |
| `X-Webhook-ID` | The delivery ID. The same across retries of one delivery. |
| `X-Webhook-Timestamp` | Milliseconds since the epoch, matching `timestamp` in the body. |
| `User-Agent` | `EditSquare-Webhooks/1.0` |

## Create a handler

Set up an HTTPS endpoint function that:

- Handles `POST` requests with a JSON payload.
- Verifies the request was sent by Edit Square, using the
  `X-Webhook-Signature` header and your endpoint's signing secret.
- Quickly returns a `2xx` status code before any long-running logic.
  For example, respond first and then download the render's output, rather
  than holding the connection open while you download it.

## Verify deliveries

Without verification, anyone who discovers your URL can `POST` fake events to
it. Always verify that a delivery came from Edit Square before acting on it.

Compute an HMAC-SHA256 over the **raw request body**, the exact bytes before
any JSON parsing, keyed with your endpoint's signing secret, and compare it to
the `X-Webhook-Signature` header in constant time:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

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

Reject any request that does not match.

If you are using a web framework, make sure it does not parse or reformat the
request body before you read it. The signature is computed over the raw bytes,
and any change to them causes verification to fail.

## Delivery behaviour

### Retries

A delivery is attempted up to **five times**: the first attempt, then four
retries with exponential backoff starting at one second. Each attempt times
out after 15 seconds, which is why your handler must respond quickly.

### Duplicate deliveries

Because retries exist, your endpoint can receive the same event more than
once. `X-Webhook-ID` is the same across retries of one delivery, so log the
IDs you have processed and skip any you have already seen.