Migration Guides2026-02-09

Webhooks Migration

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)

In the Extend dashboard, create a new webhook endpoint:

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:

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.

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:

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
  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:

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:

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:

1switch (event.eventType) {
2 // Workflow 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
11 // Extract events
12 case "extract_run.processed":
13 case "extract_run.failed":
14 // event.payload is ExtractRun
15 break;
16
17 // Classify events
18 case "classify_run.processed":
19 case "classify_run.failed":
20 // event.payload is ClassifyRun
21 break;
22
23 // Split events
24 case "split_run.processed":
25 case "split_run.failed":
26 // event.payload is SplitRun
27 break;
28
29 // Parse events
30 case "parse_run.processed":
31 case "parse_run.failed":
32 // event.payload is ParseRun
33 break;
34
35 // Edit events
36 case "edit_run.processed":
37 case "edit_run.failed":
38 // event.payload is EditRun
39 break;
40}

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