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

# Webhooks

> Receive signed events when Parse and Extract jobs finish, instead of polling for job status. Verify signatures and handle retries.

Webhooks notify your systems the moment a [Parse Job](./parse-async) or [Extract Job](./extract-async) finishes, so you don't have to poll for job status. Register an HTTPS endpoint in **[Playground](https://ade.landing.ai/)** > **Settings** > **Webhooks**, and it receives a signed POST request when a job succeeds or fails. Events carry the job's identifiers and status, never the content of the document.

## When to Use Webhooks

Any job can be polled, so webhooks matter where polling works poorly: poll frequently and most requests return `processing`, poll slowly and finished results sit unclaimed. One push per job replaces that loop. This is most valuable when many jobs run at once, when a finished parse should immediately trigger the next pipeline stage, and when failures should raise an alert rather than wait to be noticed.

## Webhook Events

The `parse` and `extract` events fire when a run reaches its final state. When the result is fetchable from the API, the event includes a `result_url`. See [Event Payload](#event-payload) for the variants. Synchronous [Parse](./parse) and [Extract](./extract) requests also fire events, but never include a `result_url`, because the result was already returned in the API response.

| Event               | When it fires                                                                                                                            |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `parse.succeeded`   | A parse run finished successfully.                                                                                                       |
| `parse.failed`      | A parse run failed.                                                                                                                      |
| `extract.succeeded` | An extract run finished successfully.                                                                                                    |
| `extract.failed`    | An extract run failed.                                                                                                                   |
| `webhook.test`      | Sent to one endpoint when you click **Send test event** in the Playground. Ignores event subscriptions. See [Test Events](#test-events). |

## How Long Results Are Available

Job results are available for 24 hours after the job completes. If your organization uses [Zero Data Retention](../ade/zdr#result-retention) (ZDR), a result is also deleted as soon as you fetch it, so the `result_url` works once.

If your pipeline may act on events late (for example, working through a backlog after downtime), fetch results promptly on receipt, or create jobs with `output_save_url` so the output is delivered to your own storage as soon as the job finishes. See [Events for Saved Output](#events-for-saved-output).

## Set Up a Webhook Endpoint

Create an endpoint on your side first: an HTTPS URL that accepts POST requests and returns a `2xx` status code. The URL must be publicly reachable. For local development, use a tunnel such as ngrok.

Then register the endpoint. Registration happens in the [Playground](https://ade.landing.ai/) only, because there is no API for managing endpoints:

1. Go to **[Playground](https://ade.landing.ai/)** > **Settings** > **Webhooks**. Endpoints belong to the workspace you create them in: your organization or your personal account.
2. Click **Add Endpoint**.
3. Enter the **Endpoint URL**.
4. (Optional) Enter a **Description** so your team knows what the endpoint is for.
5. Select the events to subscribe to. If you select none, the endpoint receives all events.
6. Click **Create endpoint**.
7. Copy the signing secret and store it securely, such as in a secret manager. The secret is shown only once. You use it to [verify event signatures](#verify-event-signatures).
8. Click **Send test event** to confirm your endpoint receives and verifies a signed event. See [Test Events](#test-events).

## Event Payload

The event body is compact JSON that identifies the job and, when the result is fetchable from the API, carries a URL for it. It never contains document content, which keeps sensitive data out of the systems around your webhooks, such as request logs and log aggregators.

```json parse.succeeded theme={null}
{
  "type": "parse.succeeded",
  "timestamp": "2026-08-14T18:02:11.482209+00:00",
  "data": {
    "job_id": "parse-01k04g2b8xv9q3m5n7r2sd4tfe",
    "status": "completed",
    "result_url": "https://api.ade.landing.ai/v2/parse/jobs/parse-01k04g2b8xv9q3m5n7r2sd4tfe",
    "result_expires_at": "2026-08-15T18:02:11.482209+00:00"
  }
}
```

| Field                    | Description                                                                                                                                                                                                                                                      |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`                   | The [event type](#webhook-events).                                                                                                                                                                                                                               |
| `timestamp`              | When the run finished (ISO 8601). Stable across delivery retries.                                                                                                                                                                                                |
| `data.job_id`            | The job identifier. For jobs, poll or fetch the job with it. Use it to correlate the event with your own records.                                                                                                                                                |
| `data.status`            | The final status: `completed` or `failed`.                                                                                                                                                                                                                       |
| `data.result_url`        | The authenticated API URL for fetching the result. The same URL you poll for [Parse Jobs](./parse-async#monitor-a-parse-job) and [Extract Jobs](./extract-async#get-a-job). Call it with your API key. Present only when the result is retrievable from the API. |
| `data.result_expires_at` | The result is not available after this time (ISO 8601). See [How Long Results Are Available](#how-long-results-are-available).                                                                                                                                   |
| `data.result_retained`   | Present and `false` only when the result can't be fetched from the API: the run was synchronous, or the output was delivered to your storage under ZDR.                                                                                                          |
| `data.delivery`          | Present only when the job saved its output to your storage. See [Events for Saved Output](#events-for-saved-output).                                                                                                                                             |

### Events for Failed Runs

Failed job events carry only the job ID and status. Fetch the job to get the error details. For a failed synchronous request, the error was already returned in the API response.

```json parse.failed theme={null}
{
  "type": "parse.failed",
  "timestamp": "2026-08-14T18:02:11.482209+00:00",
  "data": {
    "job_id": "parse-01k04g2b8xv9q3m5n7r2sd4tfe",
    "status": "failed"
  }
}
```

### Events for Saved Output

If you created the job with `output_save_url`, the event adds a `delivery` receipt confirming that the result was written to your URL, with the status code your storage returned. See [Save Parsed Output to a URL](./parse-async#save-parsed-output-to-a-url) and [Save Extraction Output to a URL](./extract-async#save-extraction-output-to-a-url).

```json extract.succeeded with saved output theme={null}
{
  "type": "extract.succeeded",
  "timestamp": "2026-08-14T18:02:11.482209+00:00",
  "data": {
    "job_id": "extract-01k04h6t8rw2xq9v3m5npbsd4f",
    "status": "completed",
    "result_url": "https://api.ade.landing.ai/v2/extract/jobs/extract-01k04h6t8rw2xq9v3m5npbsd4f",
    "result_expires_at": "2026-08-15T18:02:11.482209+00:00",
    "delivery": {
      "status": "delivered",
      "destination": "output_save_url",
      "status_code": 200
    }
  }
}
```

If your organization uses [ZDR](../ade/zdr), the result is deleted as soon as it is delivered to your `output_save_url`. The event then has no `result_url`, and `result_retained` is `false`. The `delivery` receipt is your confirmation that the job finished and where the output went. See [Result Retention](../ade/zdr#result-retention).

```json extract.succeeded with ZDR theme={null}
{
  "type": "extract.succeeded",
  "timestamp": "2026-08-14T18:02:11.482209+00:00",
  "data": {
    "job_id": "extract-01k04h6t8rw2xq9v3m5npbsd4f",
    "status": "completed",
    "result_retained": false,
    "delivery": {
      "status": "delivered",
      "destination": "output_save_url",
      "status_code": 200
    }
  }
}
```

### Test Events

Clicking **Send test event** on an endpoint sends a `webhook.test` event to that endpoint only. The event is signed like any real event, so it exercises your signature verification end to end. The `job_id` is synthetic (prefixed `test-`), there is never a `result_url`, and the send gets one attempt with no retries.

```json webhook.test theme={null}
{
  "type": "webhook.test",
  "timestamp": "2026-08-27T18:02:11.482209+00:00",
  "data": {
    "job_id": "test-01k04j8x2mv6q9r3t5w7yzbcde",
    "status": "completed",
    "result_retained": false
  }
}
```

## Verify Event Signatures

Every event is signed following the [Standard Webhooks](https://www.standardwebhooks.com/) specification, so you can confirm that an event is authentic and wasn't altered in transit. Verify the signature before acting on any event.

Each delivery includes three headers:

| Header              | Description                                                                                                                                                                                                        |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `webhook-id`        | The unique message ID: the job ID and event type joined with a period. Stable across delivery retries, so use it as your idempotency key to skip events you already processed.                                     |
| `webhook-timestamp` | When this delivery attempt was sent (Unix seconds). A fresh value per attempt.                                                                                                                                     |
| `webhook-signature` | The signature: `v1,` followed by a base64-encoded hash-based message authentication code (HMAC-SHA256) of the ID, timestamp, and raw body. Can contain multiple space-delimited signatures. Match any one of them. |

Use a Standard Webhooks library instead of comparing hashes yourself: the libraries compare in constant time and reject stale timestamps, which blocks replay attacks. Verify with the endpoint's own signing secret, and pass the raw request body exactly as received. Re-serialized JSON changes the bytes and fails verification.

<CodeGroup>
  ```python Python theme={null}
  # pip install standardwebhooks
  from standardwebhooks import Webhook

  wh = Webhook("whsec_YOUR_SIGNING_SECRET")

  def handle_webhook(raw_body: bytes, headers: dict) -> None:
      # Raises WebhookVerificationError if the signature is invalid.
      event = wh.verify(raw_body, headers)
      print(event["type"], event["data"]["job_id"])
  ```

  ```typescript TypeScript theme={null}
  // npm install standardwebhooks
  import { Webhook } from "standardwebhooks";

  const wh = new Webhook("whsec_YOUR_SIGNING_SECRET");

  function handleWebhook(rawBody: string, headers: Record<string, string>) {
    // Throws if the signature is invalid.
    const event = wh.verify(rawBody, headers) as { type: string; data: { job_id: string } };
    console.log(event.type, event.data.job_id);
  }
  ```
</CodeGroup>

## Delivery and Retries

Your endpoint has 15 seconds to respond. Return a `2xx` status code as soon as you have recorded the event, and do any heavy processing afterward. Any `2xx` counts as delivered. Any other response, or a timeout, counts as a failed attempt.

Failed deliveries are retried with increasing delays, from 30 seconds up to 2 hours between attempts: up to 14 attempts over roughly 12 hours. Retries reuse the same `webhook-id`, so deduplicating by that header makes a redelivered event harmless.

Every delivery attempt is recorded in the endpoint's delivery log in **Playground** > **Settings** > **Webhooks**, including the response status and body (truncated to 4 KB).

Responding **410 Gone** permanently cancels the remaining retries for that event. It does not unsubscribe the endpoint, and future events are still delivered. To stop all deliveries, disable or delete the endpoint in **Playground** > **Settings** > **Webhooks**.

## Troubleshoot Webhooks

Use this section to troubleshoot issues with webhook endpoints and event deliveries.

### Signature Verification Fails

Verification fails when the secret, the body, or the timestamp doesn't match what was signed. Check the three requirements in [Verify Event Signatures](#verify-event-signatures): the endpoint's own secret, the raw body bytes (web frameworks often parse and re-serialize JSON automatically), and an accurate server clock. To reproduce a delivery locally, use **Copy as curl** on the delivery in the delivery log. If you lost the endpoint's secret, delete the endpoint and create it again to get a new one.

### Endpoint Stopped Receiving Events

Check the endpoint in **Playground** > **Settings** > **Webhooks**: deliveries stop when the endpoint is disabled or deleted, or when its event subscriptions change. Review the delivery log to see what was sent and what your endpoint returned. After fixing the receiver, click **Send test event** to confirm deliveries reach it.

### Missed or Duplicate Events

If your receiver was down for longer than the retry window (roughly 12 hours), the missed deliveries are not resent. To recover, call [List Parse Jobs](./parse-async#list-parse-jobs) to find parse jobs you haven't processed, and fetch each one. Extract Jobs has no list endpoint, so keep the `job_id` from each create response on your side. Duplicates are expected with webhooks in general. Deduplicate by `webhook-id`.

### Fetching the Result Returns an Error

A `result_url` fetch returns status 410 with the error code `result_expired` when the result is no longer available. See [How Long Results Are Available](#how-long-results-are-available).
