For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
GuidesAPI ReferenceChangelogModel Versioning
GuidesAPI ReferenceChangelogModel Versioning
    • Getting Started
    • Authentication
    • API Versioning
    • SDKs
    • Deployments
    • Error Codes
    • Async Processing
  • Endpoints
  • Webhook Events
  • Migration Guides
      • Webhooks
LogoLogo
On this page
  • What’s Changing
  • Step-by-Step Migration
  • Step 1: Create a New Webhook Endpoint (Disabled)
  • Step 2: Deploy Code to Ignore New Events
  • Step 3: Deploy Code to Process New Events
  • Step 4: Monitor and Validate
  • Step 5: Disable the Old Webhook Endpoint
  • SDK Webhook Helpers
  • Available Methods
  • Handling Signed URL Payloads
  • All Event Types
  • Security Best Practices
  • Need Help?
  • Migration Guides
Migration Guides2026-02-09

Webhooks Migration

Was this page helpful?
Previous

Changelog

Next
Built with

What’s Changing

Event types now use the new resource names:

Old Event TypeNew Event Type
processor_run.processed (type: EXTRACT)extract_run.processed
processor_run.failed (type: EXTRACT)extract_run.failed
processor_run.processed (type: CLASSIFY)classify_run.processed
processor_run.failed (type: CLASSIFY)classify_run.failed
processor_run.processed (type: SPLITTER)split_run.processed
processor_run.failed (type: SPLITTER)split_run.failed
parser_run.processedparse_run.processed
parser_run.failedparse_run.failed

Payload structures also change to match the new API schemas and are documented in the API Reference.


Step-by-Step Migration

This migration pattern allows you to test the new webhook format while keeping your existing integration working, with the ability to rollback at any point.

Step 1: Create a New Webhook Endpoint (Disabled)

Create a new webhook endpoint either in the Extend dashboard or via the API (POST /webhook_endpoints + POST /webhook_subscriptions):

SettingValue
URLSame URL with a version query parameter, e.g., https://example.com/webhooks?api_version=2026-02-09
API Version2026-02-09
EventsSame events, using new names (e.g., extract_run.processed instead of processor_run.processed)
StatusDisabled (don’t enable yet)

The query parameter lets your code distinguish between events from the old and new endpoints.

State after Step 1:

Old endpoint (2025-04-21): ✅ Enabled, processing events
New endpoint (2026-02-09): ❌ Disabled

Step 2: Deploy Code to Ignore New Events

Update your webhook handler to acknowledge but ignore events from the new endpoint:

TypeScript
Python
Java
1async function handleWebhook(req: Request) {
2 const event = req.body;
3 const apiVersion = req.query.api_version;
4
5 // Ignore new version events (for now)
6 if (apiVersion === "2026-02-09") {
7 console.log("New webhook format received (ignoring):", event.eventType);
8 return 200; // Acknowledge but don't process
9 }
10
11 // Process old version normally
12 if (await hasProcessedEvent(event.id)) {
13 return 200; // Idempotent
14 }
15
16 await processOldWebhookEvent(event);
17 await markEventProcessed(event.id);
18 return 200;
19}

Then enable the new webhook endpoint (in the dashboard or via POST /webhook_endpoints/{id}).

State after Step 2:

Old endpoint (2025-04-21): ✅ Enabled, processing events
New endpoint (2026-02-09): ✅ Enabled, acknowledged but ignored
Both endpoints receive events, only old is processed.

This lets you verify:

  • New endpoint is receiving events
  • Payload format matches documentation
  • No errors in basic parsing

Step 3: Deploy Code to Process New Events

Update your handler to process new events and reject old ones:

TypeScript
Python
Java
1async function handleWebhook(req: Request) {
2 const event = req.body;
3 const apiVersion = req.query.api_version;
4
5 // Reject old version (triggers retry for rollback safety)
6 if (apiVersion === "2025-04-21") {
7 console.log("Old webhook format (rejecting):", event.eventType);
8 return 400; // Reject so Extend retries if we need to rollback
9 }
10
11 // Process new version
12 if (await hasProcessedEvent(event.id)) {
13 return 200; // Idempotent
14 }
15
16 await processNewWebhookEvent(event);
17 await markEventProcessed(event.id);
18 return 200;
19}

Return 400 for old events. This causes Extend to retry delivery. If you need to rollback, those events will be re-delivered when you revert your code.

State after Step 3:

Old endpoint (2025-04-21): ✅ Enabled, rejected (returns 400, retries queued)
New endpoint (2026-02-09): ✅ Enabled, processing events
Both endpoints receive events, only new is processed.

Step 4: Monitor and Validate

Monitor your new webhook processing for:

  • Processing errors
  • Unexpected payload formats
  • Business logic correctness

If issues arise:

  1. Revert to Step 2 code (ignore new, process old)
  2. Temporarily disable the new webhook endpoint
  3. Old endpoint will process the retried events (because you returned 400)
  4. Investigate and fix issues
  5. Re-enable new endpoint and resume from Step 3

Step 5: Disable the Old Webhook Endpoint

Once you’re confident in the new endpoint:

  1. Disable the old webhook endpoint (in the dashboard or via POST /webhook_endpoints/{id})
  2. Remove the version check from your code
  3. Clean up old event processing logic

State after Step 5:

Old endpoint (2025-04-21): ❌ Disabled
New endpoint (2026-02-09): ✅ Enabled, processing events
Migration complete!

SDK Webhook Helpers

The SDK includes utilities for verifying signatures and parsing events with type-safe payloads:

TypeScript
Python
Java
1import { ExtendClient, WebhookSignatureVerificationError } from "extend-ai";
2
3const client = new ExtendClient({ token: process.env.EXTEND_API_KEY! });
4
5app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
6 try {
7 const event = client.webhooks.verifyAndParse(
8 req.body.toString(),
9 req.headers,
10 process.env.EXTEND_WEBHOOK_SECRET!
11 );
12
13 switch (event.eventType) {
14 case "extract_run.processed":
15 console.log("Output:", event.payload.output);
16 break;
17 case "workflow_run.completed":
18 console.log("Workflow done:", event.payload.id);
19 break;
20 }
21
22 res.status(200).send("OK");
23 } catch (err) {
24 if (err instanceof WebhookSignatureVerificationError) {
25 res.status(401).send("Invalid signature");
26 }
27 }
28});

Available Methods

MethodDescription
verifyAndParse()Verify signature and parse event in one call
verify()Verify signature only (returns boolean)
parse()Parse event without verification
isSignedUrlEvent()Check for signed URL payloads
fetchSignedPayload()Fetch full payload from signed URL

Handling Signed URL Payloads

For large payloads (e.g., workflow runs with many documents), Extend delivers webhooks with a signed URL instead of the full payload. This keeps webhook delivery fast and reliable.

By default, verifyAndParse() throws an error if a signed URL payload is received. To handle these, opt-in with allowSignedUrl:

TypeScript
Python
Java
1const event = client.webhooks.verifyAndParse(body, headers, secret, {
2 allowSignedUrl: true
3});
4
5if (client.webhooks.isSignedUrlEvent(event)) {
6 console.log("Resource ID:", event.payload.id);
7 const fullEvent = await client.webhooks.fetchSignedPayload(event);
8 console.log("Full payload:", fullEvent.payload);
9}

Signed URLs expire after 1 hour. Fetch the payload promptly.


All Event Types

Use the eventType field for type-safe payload access. See the Webhook Events reference for the complete catalog and payload schemas.

1switch (event.eventType) {
2 // Workflow run events
3 case "workflow_run.completed":
4 case "workflow_run.failed":
5 case "workflow_run.needs_review":
6 case "workflow_run.rejected":
7 case "workflow_run.cancelled":
8 // event.payload is WorkflowRun
9 break;
10 case "workflow_run.step_run.processed":
11 // event.payload is a single StepRun (incremental step results)
12 break;
13
14 // Workflow lifecycle events
15 case "workflow.created":
16 case "workflow.deployed":
17 case "workflow.deleted":
18 // event.payload is Workflow
19 break;
20
21 // Extract run events
22 case "extract_run.processed":
23 case "extract_run.failed":
24 // event.payload is ExtractRun
25 break;
26
27 // Extractor lifecycle events
28 case "extractor.created":
29 case "extractor.updated":
30 case "extractor.deleted":
31 case "extractor.draft.updated":
32 case "extractor.version.published":
33 // event.payload is Extractor / ExtractorVersion
34 break;
35
36 // Classify run events
37 case "classify_run.processed":
38 case "classify_run.failed":
39 // event.payload is ClassifyRun
40 break;
41
42 // Classifier lifecycle events
43 case "classifier.created":
44 case "classifier.updated":
45 case "classifier.deleted":
46 case "classifier.draft.updated":
47 case "classifier.version.published":
48 // event.payload is Classifier / ClassifierVersion
49 break;
50
51 // Split run events
52 case "split_run.processed":
53 case "split_run.failed":
54 // event.payload is SplitRun
55 break;
56
57 // Splitter lifecycle events
58 case "splitter.created":
59 case "splitter.updated":
60 case "splitter.deleted":
61 case "splitter.draft.updated":
62 case "splitter.version.published":
63 // event.payload is Splitter / SplitterVersion
64 break;
65
66 // Parse events
67 case "parse_run.processed":
68 case "parse_run.failed":
69 // event.payload is ParseRun
70 break;
71
72 // Edit events
73 case "edit_run.processed":
74 case "edit_run.failed":
75 // event.payload is EditRun
76 break;
77
78 // Batch events
79 case "batch_processor_run.processed":
80 case "batch_processor_run.failed":
81 // event.payload is BatchProcessorRun (extract/classify/split batches)
82 break;
83 case "batch_parse_run.processed":
84 case "batch_parse_run.failed":
85 // event.payload is BatchParseRun
86 break;
87}

Security Best Practices

  1. Always verify signatures — Never skip verification in production
  2. Use environment variables — Store your webhook secret securely
  3. Use raw body — Parse the body as a string before verification
  4. Implement idempotency — Use eventId to handle duplicate deliveries
  5. Respond quickly — Return 200 before heavy processing
TypeScript
Python
Java
1app.post("/webhook", async (req, res) => {
2 const event = client.webhooks.verifyAndParse(body, headers, secret);
3
4 // Acknowledge quickly
5 res.status(200).send("OK");
6
7 // Process asynchronously
8 processWebhookAsync(event).catch(console.error);
9});

Need Help?

If you encounter any issues while migrating your webhook handlers, please contact our support team at support@extend.app.


Migration Guides

GuideMigrating FromMigrating To
Overview—What’s new and how to upgrade
Extract Runs/processor_runs/extract_runs + /extract
Classify Runs/processor_runs/classify_runs + /classify
Split Runs/processor_runs/split_runs + /split
Parse Runs/parse, /parse/async/parse_runs + /parse
Edit Runs/edit, /edit/async/edit_runs + /edit
Extractors/processors/extractors
Classifiers/processors/classifiers
Splitters/processors/splitters
Files/files/files (breaking changes)
Evaluation Setsevaluation endpointsUpdated evaluation endpoints
Workflow Runs/workflow_runs/workflow_runs (breaking changes)
Webhooksprocessor_run.* eventsextract_run.*, classify_run.*, etc.