Skip to main content
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.
The following 6-minute video shows how to use the Unstructured API to work with webhooks:
Visit the Unstructured documentation samples repo 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 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 in the Postman Docs.
For Unstructured webhooks, Unstructured is the sender, and your solution is the receiver. Some popular receiver solutions include AWS Lambda, Azure Functions, Google Cloud Run, Zapier, Slack, Svix, and 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 that is associated with its target workflow 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 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.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. Other events (job.scheduled, job.in_progress, job.stopped) are sent immediately. 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.
    Each Let’s Go or Pay-As-You-Go account has one and only one workspace.An Unstructured Business account can have multiple workspaces.
  • 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.
Unstructured Pipelines allows only limited creation, viewing, and management of webhooks, as follows (learn how):
  • 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.

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

FieldTypeDescription
event_idstringUnique identifier for this notification delivery.
typestringEvent type: job.scheduled, job.in_progress, job.stopped, job.completed, or job.failed.
timestampstringISO 8601 UTC timestamp when the event was dispatched.
tenant_idstringUUID of the Unstructured account that owns the job.
jobobjectJob details. See the job object fields table below.

Job object fields

FieldTypePresent onDescription
job_idstringAll eventsUUID of the job.
workflow_idstringAll eventsUUID of the workflow the job belongs to.
workflow_namestringAll eventsDisplay name of the workflow.
statusstringAll eventsJob status at time of event: SCHEDULED, IN_PROGRESS, STOPPED, COMPLETED, or FAILED.
job_typestringAll eventsHow the job was triggered: scheduled or ephemeral.
created_atstring or nullAll eventsISO 8601 UTC timestamp when the job was created.
completed_atstring or nullAll eventsISO 8601 UTC timestamp when the job reached a terminal state. null for non-terminal events.
processing_time_msinteger or nullAll eventsElapsed time from creation to terminal state, in milliseconds. null for non-terminal events.
num_docsinteger or nullAll eventsTotal files discovered for the job. null until counts are reported.
num_docs_succeededinteger or nullAll eventsFiles that processed successfully. null until counts are reported.
num_docs_failedinteger or nullAll eventsFiles that failed to process. null until counts are reported.
counts_finalbooleanjob.completed, job.failedtrue 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.
reasonstringjob.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.
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.

Examples

All files processed successfully. num_docs_succeeded equals num_docs, num_docs_failed is 0, and counts_final is true.
{
  "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
  }
}
num_docs_failed is set to 1, indicating one file failed while the rest succeeded. The job still reports as job.completed.
{
  "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
  }
}
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.
{
  "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
  }
}
The job could not start. No files were processed, so num_docs fields are null and counts_final is false.
{
  "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
  }
}

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.
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") 
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.

Requirements

To create, view, and manage webhooks, Unstructured provides a set of Representational State Transfer (REST) endpoints. 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. After you sign up, you are automatically signed in to your new Unstructured Let’s Go account, at https://platform.unstructured.io.
    To sign up for a Business account instead, contact Unstructured Sales, or learn more.
  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.
    For other types of Business accounts, see your Unstructured account administrator for sign-in instructions, or request support.
  3. Get your Unstructured API URL:
    a. After you sign in to your Unstructured Let’s Go, Pay-As-You-Go, or Business account, click API Keys on the sidebar.
    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.
    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. After you sign up, you are automatically signed in to your new Unstructured Let’s Go account, at https://platform.unstructured.io.
    To sign up for a Business account instead, contact Unstructured Sales, or learn more.
  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.
    For other types of Business accounts, see your Unstructured account administrator for sign-in instructions, or request support.
  3. Get your Unstructured API key:
    a. After you sign in to your Unstructured Let’s Go, Pay-As-You-Go, or Business account, click API Keys on the sidebar.
    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.
    b. Click Generate API Key.
    c. Follow the on-screen instructions to finish generating the key.
    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.
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!
For more information on workspace-level webhook operations, see Notifications. For more information on workflow-level webhook operations, see Workflows.