Skip to main content

When to use webhooks

Webhooks are better than polling when:
  • You have many concurrent submissions
  • You want real-time notifications
  • Your API server resources are limited
  • You prefer an event-driven architecture
Polling is fine for:
  • Occasional submissions
  • Simple scripts and CLIs
  • Quick prototypes and testing

Setting up webhooks

Step 1: Prepare your endpoint

Create an HTTPS endpoint that can receive POST requests. The delivered envelope uses Standard Webhooks, so verify the signature with the standardwebhooks library and your subscription secret. The verified event exposes type (the event type) and data (a snake_case object).

Step 2: Create a subscription

Use POST /api/webhooks/subscriptions. The request fields are endpointUrl (HTTPS required), eventTypes, and an optional description.
Response (201):
The secret is returned only on create. Save it immediately — it is never shown again. The field is secret (not signing_secret), the enabled flag is enabled (not active), and the event list is eventTypes (not events).
A non-HTTPS endpointUrl or hitting your workspace subscription limit returns 422.

Step 3: Test your endpoint

Create a submission and watch your endpoint receive events:

Webhook events

There are exactly two event types. The delivered envelope has id, type, timestamp, and a snake_case data object. The payload does not embed results — fetch detail by ID after receiving an event.

asset.processing.completed

Fires when an asset finishes processing successfully.
data.external_reference is echoed verbatim from the submission’s externalReference and is present only when it was set at create (the key is omitted otherwise). It also appears on asset.processing.failed deliveries. See Webhooks → delivery payload.

asset.processing.failed

Fires when an asset fails to process. The data object adds error_message and is_retryable — when true, the asset can be retried via the retry endpoint; when false, the failure is permanent.

Handling webhooks

Basic handler pattern

Read event["type"] and the snake_case data fields. Because the payload carries no results, fetch issues or topics by asset ID when you need detail.
If you prefer clustered topics instead of a flat issue list, fetch .../assets/{asset_id}/topics the same way.

Queue-based processing

For high-volume scenarios, queue webhooks for async processing and respond immediately:

Webhook reliability

Signature verification

Always verify webhook signatures with the subscription secret:

Idempotency

Deliveries may arrive more than once. The delivery provider retries failed deliveries, so make your handler idempotent by deduplicating on the event id:
Store processed event IDs in your database:
Failed deliveries are retried automatically by the delivery provider. Check the deliveries endpoint (GET .../subscriptions/{id}/deliveries) or the control plane for attempt history rather than assuming a fixed retry schedule.

Testing webhooks locally

Using ngrok

Expose your local server publicly:
This gives you a URL like https://abc123.ngrok.io. Use it as the endpointUrl in your subscription.

Managing subscriptions

The list endpoint returns a paginated envelope: { "items": [...], "page": 1, "pageSize": 10, "total": 3, "totalPages": 1 }. None of these responses include the secret — it is returned only on create.

Complete example

An end-to-end handler with verification, idempotency, and background processing:

Best practices

  • Verify signatures on every webhook with the subscription secret.
  • Respond quickly and process asynchronously in the background.
  • Deduplicate on the event id to handle repeated deliveries.
  • Fetch issues or topics by asset ID — the payload carries no inline results.
  • Log all webhook events for debugging.
  • Monitor delivery status via the deliveries endpoint or the control plane.