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

# Webhooks

A *webhook* is a common technique that allows a *sender* to automatically send data to a *receiver* based on events that happen in the sender.
Some common uses for webhooks include sending real-time notifications such as emails, pages, or alerts to messaging apps; automatically
triggering build, test, or deployment workflows in CI/CD pipelines; launching downstream data synchronization updates; or triggering agentic AI workflows.

The following 3-minute video provides an overview of webhooks.

<iframe width="560" height="315" src="https://www.youtube.com/embed/zX_8qAzw000" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen />

The following 6-minute video shows how to use the Unstructured API to work with webhooks:

<iframe width="560" height="315" src="https://www.youtube.com/embed/rc3aKH2YcLE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen />

<Tip>
  Visit the [Unstructured documentation samples repo](https://github.com/Unstructured-IO/docs-samples/tree/main/postman) on GitHub for Postman Collections that demonstrate the API requests discussed in this topic. To import any of these Postman collections:

  1. Browse to the [postman folder](https://github.com/Unstructured-IO/docs-samples/tree/main/postman) in the repo, and select the collection file you want to import.
  2. Click the **Raw** icon, and then copy the URL for the raw JSON file.
  3. In your Postman workspace, click **Import**, and then paste in the file URL.

  To learn about Postman Collections, see [Manage and organize Postman Collections](https://learning.postman.com/latest-v-12/docs/use/use-collections/manage-collections) in the Postman Docs.
</Tip>

For Unstructured webhooks, Unstructured is the sender,
and your solution is the receiver. Some popular receiver solutions include
[AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/urls-webhook-tutorial.html),
[Azure Functions](https://learn.microsoft.com/azure/azure-functions/functions-bindings-http-webhook),
[Google Cloud Run](https://docs.cloud.google.com/run/docs/triggering/webhooks),
[Zapier](https://zapier.com/blog/what-are-webhooks/),
[Slack](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/),
[Svix](https://svix.com), and
[Webhook.site](https://webhook.site/).

The receiver provides a unique URL, called the
*webhook URL*, to receive the data that Unstructured sends.
Behind the scenes, when specific events happen in Unstructured, Unstructured automatically calls the webhook URL by using an HTTP POST operation and
passes JSON payloads related to those events to the receiver. The receiver then processes the payload and decides what to do with the data.

Because webhooks are event-driven, some event must first be triggered to begin generating the related data to be sent. In
Unstructured, these webhooks can be triggered whenever a [job](/api-reference/workflow/jobs) that is associated with its target [workflow](/api-reference/workflow/workflows) does
one or more of the following:

* Is scheduled to start running later (programmatically, an event type of `job.scheduled`)
* Has begun running (`job.in_progress`)
* Has reached approximately 25% progress (`job.progress`)
* Has stopped running (`job.stopped`)
* Has failed (`job.failed`) - the job either failed to initialize and did not process any files, or all files failed to process. When all files fail, the payload includes `reason: "All files failed to process"` and `counts_final: true`.
* Has completed (`job.completed`) - the job initialized and finished processing, including cases where some (but not all) files failed. The payload includes `num_docs`, `num_docs_succeeded`, `num_docs_failed`, and `counts_final`.

Unstructured sends the `job.progress` event only for jobs with at least four files. For concurrent jobs in the same workflow, Unstructured consolidates progress notifications and might send only one `job.progress` event.

Unstructured sends the `job.completed` and `job.failed` HTTP POST approximately 10 seconds after a job finishes, allowing time for per-file count fields in the payload to reflect the final result. Unstructured sends the other events (`job.scheduled`, `job.in_progress`, `job.progress`, and `job.stopped`) as soon as they occur.

When a webhook is configured to deliver event-driven notification data payloads from a sender to a receiver, this configuration
is called a *notification channel*. For Unstructured webhooks, notification channels can be created and managed at the following levels:

* At the **workspace** level, known as a *workspace-scoped notification channel*. This allows any job that is associated with a
  workspace in an Unstructured account to trigger the webhook. This can be useful, for example, for routing pager requests to a team of on-call engineers in an
  IT operations center whenever a job fails across the workspace.

  <Note>
    Each **Let's Go** or **Pay-As-You-Go** account has one and only one workspace.

    An Unstructured **Business** account can have multiple workspaces.
  </Note>

* At the **workflow** level, known as a *workflow-scoped notification channel*. This allows any job that is associated only with
  the target workflow in an Unstructured account to trigger the webhook. This can be useful, for example, for emailing a department's data analyst when a long-running job for a specific workflow
  has completed, allowing them to begin working with the latest output.

Whenever a webhook is triggered, the act of Unstructured sending the event data payload is called a
*notification*. You can get a count or a list of these notifications. You can also mark them in your Unstructured account as read after
you have processed them according to your organization's needs. Marking a notification as read can help you keep track of
which notifications you have already dealt with and which ones you still need to take action on.

<Info>
  Unstructured Pipelines allows only limited creation, viewing, and management of webhooks, as follows ([learn how](/pipelines/webhooks)):

  * Webhooks for a personal Unstructured workspace. Each personal Unstructured account has one and only one personal Unstructured workspace.
  * Webhooks for a workspace within an Unstructured **Business** account.

  You cannot use Unstructured Pipelines to create, view, or manage notifications or workflow-level webhooks.
</Info>

## Payload schema and examples

Every webhook delivers an HTTP POST with a JSON body containing a top-level envelope and a `job` object with event-specific details.

### Top-level fields

| Field       | Type   | Description                                                                                                      |
| ----------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `event_id`  | string | Unique identifier for this notification delivery.                                                                |
| `type`      | string | Event type: `job.scheduled`, `job.in_progress`, `job.progress`, `job.stopped`, `job.completed`, or `job.failed`. |
| `timestamp` | string | ISO 8601 UTC timestamp when the event was dispatched.                                                            |
| `tenant_id` | string | UUID of the Unstructured account that owns the job.                                                              |
| `job`       | object | Job details. See the [job object fields](#job-object-fields) table below.                                        |

### Job object fields

| Field                | Type            | Present on                      | Description                                                                                                                                                                                                                            |
| :------------------- | :-------------- | :------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `job_id`             | string          | All events                      | UUID of the job.                                                                                                                                                                                                                       |
| `workflow_id`        | string          | All events                      | UUID of the workflow the job belongs to.                                                                                                                                                                                               |
| `workflow_name`      | string          | All events                      | Display name of the workflow.                                                                                                                                                                                                          |
| `status`             | string          | All events                      | Job status at time of event: `SCHEDULED`, `IN_PROGRESS`, `STOPPED`, `COMPLETED`, or `FAILED`. A `job.progress` event has the `IN_PROGRESS` status.                                                                                     |
| `job_type`           | string          | All events                      | How the job was triggered: `scheduled` or `ephemeral`.                                                                                                                                                                                 |
| `created_at`         | string or null  | All events                      | ISO 8601 UTC timestamp when the job was created.                                                                                                                                                                                       |
| `completed_at`       | string or null  | All events                      | ISO 8601 UTC timestamp when the job reached a terminal state. `null` for non-terminal events.                                                                                                                                          |
| `processing_time_ms` | integer or null | All events                      | Elapsed time from creation to terminal state, in milliseconds. `null` for non-terminal events.                                                                                                                                         |
| `num_docs`           | integer or null | All events                      | Total files discovered for the job. `null` until counts are reported.                                                                                                                                                                  |
| `num_docs_succeeded` | integer or null | All events                      | Files that processed successfully. `null` until counts are reported.                                                                                                                                                                   |
| `num_docs_failed`    | integer or null | All events                      | Files that failed to process. `null` until counts are reported.                                                                                                                                                                        |
| `files_processed`    | integer         | `job.progress`                  | Files processed when the job reached approximately 25% progress.                                                                                                                                                                       |
| `source_connector`   | string or null  | `job.progress`                  | Source connector type for the workflow.                                                                                                                                                                                                |
| `counts_final`       | boolean         | `job.completed`, `job.failed`   | `true` if all file processing counts are complete. `false` if the polling window closed before all file processing counts were accounted for. Only present on `job.completed` and `job.failed` events.                                 |
| `reason`             | string          | `job.failed` (all files failed) | Reason for failure. Value: `"All files failed to process"`. Only present when all files failed, not set for init failures. Check `num_docs` to distinguish the two cases: `null` indicates the job failed before processing any files. |

<Note>
  When `counts_final` is `false`, the polling window closed before all file processing counts were accounted for. Unstructured determines completeness by checking whether `num_docs_succeeded + num_docs_failed` equals `num_docs`. To confirm final counts, call [Get processing details for a job](/api-reference/api/job/get-job-details).
</Note>

### Examples

<AccordionGroup>
  <Accordion title="job.completed — all files succeeded">
    All files processed successfully. `num_docs_succeeded` equals `num_docs`, `num_docs_failed` is `0`, and `counts_final` is `true`.

    ```json theme={null}
    {
      "event_id": "2d4f6a8c-1b3e-5d7f-9a0b-2c4e6f8a0b2d",
      "type": "job.completed",
      "timestamp": "2025-01-15T10:00:05.123456+00:00",
      "tenant_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "job": {
        "job_id": "550e8400-e29b-41d4-a716-446655440003",
        "workflow_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "workflow_name": "Ingest quarterly reports",
        "status": "COMPLETED",
        "job_type": "scheduled",
        "created_at": "2025-01-15T09:58:00.000000+00:00",
        "completed_at": "2025-01-15T09:59:55.000000+00:00",
        "processing_time_ms": 115000,
        "num_docs": 16,
        "num_docs_succeeded": 16,
        "num_docs_failed": 0,
        "counts_final": true
      }
    }
    ```
  </Accordion>

  <Accordion title="job.completed — partial failure (some files failed)">
    `num_docs_failed` is set to `1`, indicating one file failed while the rest succeeded. The job still reports as `job.completed`.

    ```json theme={null}
    {
      "event_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "type": "job.completed",
      "timestamp": "2025-01-15T10:00:05.123456+00:00",
      "tenant_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "job": {
        "job_id": "550e8400-e29b-41d4-a716-446655440000",
        "workflow_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "workflow_name": "Ingest quarterly reports",
        "status": "COMPLETED",
        "job_type": "scheduled",
        "created_at": "2025-01-15T09:58:00.000000+00:00",
        "completed_at": "2025-01-15T09:59:55.000000+00:00",
        "processing_time_ms": 115000,
        "num_docs": 16,
        "num_docs_succeeded": 15,
        "num_docs_failed": 1,
        "counts_final": true
      }
    }
    ```
  </Accordion>

  <Accordion title="job.failed — all files failed">
    Every file in the job failed to process. `num_docs_failed` equals `num_docs`, `reason` is set, and `counts_final: true` confirms the counts are complete.

    ```json theme={null}
    {
      "event_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "type": "job.failed",
      "timestamp": "2025-01-15T10:00:05.123456+00:00",
      "tenant_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "job": {
        "job_id": "550e8400-e29b-41d4-a716-446655440001",
        "workflow_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "workflow_name": "Ingest quarterly reports",
        "status": "FAILED",
        "reason": "All files failed to process",
        "job_type": "scheduled",
        "created_at": "2025-01-15T09:58:00.000000+00:00",
        "completed_at": "2025-01-15T09:59:55.000000+00:00",
        "processing_time_ms": 115000,
        "num_docs": 10,
        "num_docs_succeeded": 0,
        "num_docs_failed": 10,
        "counts_final": true
      }
    }
    ```
  </Accordion>

  <Accordion title="job.failed — failed to initialize">
    The job could not start. No files were processed, so `num_docs` fields are `null` and `counts_final` is `false`.

    ```json theme={null}
    {
      "event_id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed",
      "type": "job.failed",
      "timestamp": "2025-01-15T10:00:02.000000+00:00",
      "tenant_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "job": {
        "job_id": "550e8400-e29b-41d4-a716-446655440002",
        "workflow_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
        "workflow_name": "Ingest quarterly reports",
        "status": "FAILED",
        "job_type": "scheduled",
        "created_at": "2025-01-15T09:59:58.000000+00:00",
        "completed_at": "2025-01-15T10:00:00.000000+00:00",
        "processing_time_ms": 2000,
        "num_docs": null,
        "num_docs_succeeded": null,
        "num_docs_failed": null,
        "counts_final": false
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Verify webhook requests

Your receiver application can verify incoming webhook requests to check that the payload originated from Unstructured and was not tampered with in transit. Use the webhook secret to verify incoming requests. If you suspect that the secret has been compromised, rotate it to a new value.

The following Python example reads the webhook ID, timestamp, and signature from the request headers and checks that the timestamp is within an acceptable time window.  Next, it recomputes the expected HMAC-SHA256 signature from the webhook ID, timestamp, and raw request body using the notification channel secret. It then compares the computed signature to the versioned signature in the request header. If the values match, the request can be trusted as originating from Unstructured and as not having been tampered with in transit.

<Accordion title="Python">
  ```python theme={null}
  import hashlib
  import hmac
  import base64
  import time


  def verify_webhook(
      payload: bytes, headers: dict, secret: str, tolerance_seconds: int = 300
  ) -> bool:
      msg_id = headers["webhook-id"]
      timestamp = headers["webhook-timestamp"]
      signature_header = headers["webhook-signature"]

      # Reject stale timestamps
      if abs(time.time() - int(timestamp)) > tolerance_seconds:
          raise ValueError("Timestamp too old")

      # Use the secret as the signing key
      secret_bytes = secret.encode("utf-8")

      # Sign: "{msg_id}.{timestamp}.{raw_body}"
      signed_content = f"{msg_id}.{timestamp}.".encode() + payload
      expected = base64.b64encode(
          hmac.new(secret_bytes, signed_content, hashlib.sha256).digest()
      ).decode()

      # The header may contain multiple space-separated versioned sigs
      for sig in signature_header.split(" "):
          version, value = sig.split(",", 1)
          if version == "v1" and hmac.compare_digest(expected, value):
              return True

      raise ValueError("Invalid signature") 
  ```
</Accordion>

<Warning>
  Use the raw request body exactly as received, not a re-serialized JSON payload. Using a re-serialized payload can cause signature verification to fail.
</Warning>

## Requirements

To create, view, and manage webhooks, Unstructured provides a [set of Representational State Transfer (REST) endpoints](/api-reference/api/notification/notification-apis). You can
call these endpoints through standard REST-enabled utilities, tools, programming languages, packages, and libraries.

To call the Unstructured API's webhook operations, you must have an Unstructured API URL and a valid Unstructured API key.

To get your Unstructured API URL, do the following:

1. If you do not already have an Unstructured account, [sign up for free](https://unstructured.io/?modal=try-for-free).
   After you sign up, you are automatically signed in to your new Unstructured **Let's Go** account, at [https://platform.unstructured.io](https://platform.unstructured.io).

   <Note>
     To sign up for a **Business** account instead, [contact Unstructured Sales](https://unstructured.io/?modal=contact-sales), or [learn more](/api-reference/overview#pricing).
   </Note>

2. If you have an Unstructured **Let's Go**, **Pay-As-You-Go**, or **Business SaaS** account and are not already signed in, sign in to your account at [https://platform.unstructured.io](https://platform.unstructured.io).

   <Note>
     For other types of **Business** accounts, see your Unstructured account administrator for sign-in instructions,
     or [request support](support/request).
   </Note>

3. Get your Unstructured API URL:<br />

   a. After you sign in to your Unstructured **Let's Go**, **Pay-As-You-Go**, or **Business** account, click **API Keys** on the sidebar.<br />

   <Note>
     For a **Business** account, before you click **API Keys**, make sure you have selected the organizational workspace you want to get the API URL for.
     [Learn more](https://docs.unstructured.io/pipelines/account/workspaces#access-a-workspace).
   </Note>

   b. Copy the value of **Unstructured API Endpoint** to your system's clipboard.

To get your Unstructured API key, do the following:

1. If you do not already have an Unstructured account, [sign up for free](https://unstructured.io/?modal=try-for-free).
   After you sign up, you are automatically signed in to your new Unstructured **Let's Go** account, at [https://platform.unstructured.io](https://platform.unstructured.io).

   <Note>
     To sign up for a **Business** account instead, [contact Unstructured Sales](https://unstructured.io/?modal=contact-sales), or [learn more](/api-reference/overview#pricing).
   </Note>

2. If you have an Unstructured **Let's Go**, **Pay-As-You-Go**, or **Business SaaS** account and are not already signed in, sign in to your account at [https://platform.unstructured.io](https://platform.unstructured.io).

   <Note>
     For other types of **Business** accounts, see your Unstructured account administrator for sign-in instructions,
     or [request support](support/request).
   </Note>

3. Get your Unstructured API key:<br />

   a. After you sign in to your Unstructured **Let's Go**, **Pay-As-You-Go**, or **Business** account, click **API Keys** on the sidebar.<br />

   <Note>
     For a **Business** account, before you click **API Keys**, make sure you have selected the organizational workspace you want to create an API key
     for. Each API key works with one and only one organizational workspace. [Learn more](/pipelines/account/workspaces#create-an-api-key-for-a-workspace).
   </Note>

   b. Click **Generate API Key**.<br />
   c. Follow the on-screen instructions to finish generating the key.<br />
   d. Click the **Copy** icon next to your new key to add the key to your system's clipboard. If you lose this key, simply return and click the **Copy** icon again.<br />

<Tip>
  Each Unstructured API key works with one and only one workspace (and the workflows within that workspace). Make sure you are using the right API key
  for your target workspace!
</Tip>

For more information on workspace-level webhook operations, see [Notifications](/api-reference/api/notification/notification-apis). For more information on workflow-level webhook operations, see [Workflows](/api-reference/api/workflow/workflow-apis).
