Webhooks

A webhook is an HTTPS endpoint you register once for your organization. Extralt POSTs an event to it each time work finishes, whether the work was started from the dashboard, the Agent, the API, or a Schedule. Use webhooks to trigger a workflow or to stop polling a Run.

Events are signed with Standard Webhooks, so you can verify them with an existing library in most languages.

Event types

Each event is sent when a resource reaches a final status. Its data is the same object the API returns for that resource.

Event typeSent whendata
robot.completedA Robot build completedRobot (GET /v1/extract/robots/{id})
robot.failedA Robot build failedRobot
run.completedA Run completedRun (GET /v1/extract/runs/{id})
run.stoppedA Run was stoppedRun
run.failedA Run failedRun
run.enrichment_skippedA Run with auto_enrich completed, but its automatic Enrichment could not startRun
enrichment.completedAn Enrichment completedEnrichment (GET /v1/enrich/enrichments/{id})
enrichment.stoppedAn Enrichment was stoppedEnrichment
enrichment.failedAn Enrichment failedEnrichment
enrichment.changesAn Enrichment found price, stock or Listing changesEnrichment, with its changes counts
import.completedAn Import completedImport (GET /v1/extract/imports/{id})
import.failedAn Import failed, including failed validationImport
webhook.testYou sent a test eventA fixed sample

A restarted Run sends a new event each time it finishes, so a Run that failed, was restarted, and then completed sends run.failed and then run.completed. run.enrichment_skipped follows the Run's run.completed event; it does not replace it.

enrichment.changes follows an Enrichment's final status when its Items changed anything: price decreases and increases, new lows, out of stock, restocked, and Listings added or removed. Its changes object counts them with the definitions of /v1/explore/activity. No event is sent when nothing changed, and never one event per changed price.

New event types are added over time. An endpoint created without event_types receives them as they arrive, so ignore types your handler does not know rather than rejecting them.

Manage webhooks

Webhooks are managed in the dashboard under Settings > Connections, or through the API with an API key. Automation tools such as n8n and Zapier create and delete their own webhooks when you switch a workflow on or off.

Dashboard

Only organization admins manage webhooks in the dashboard. The Webhooks section of the Connections page lists every endpoint with its status and last delivery. From there you can add an endpoint, reveal or rotate its signing secret, enable or disable it, edit its URL, description, or events, send a test event, and review recent deliveries with their response codes. A finished delivery can be redelivered with the same id and body.

Create a webhook

curl -s -X POST "https://api.extralt.com/v1/webhooks" \
  -H "Authorization: Bearer $EXTRALT_API_KEY" \
  -H "Idempotency-Key: webhook-create-price-alerts" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/extralt",
    "event_types": ["run.completed", "run.failed"],
    "description": "Price alerts workflow"
  }'

The response is 201 Created with the webhook and its signing secret:

{
  "id": "pn74h2q9x8c3v1m6z5t0r7wbk2f8djsa",
  "url": "https://hooks.example.com/extralt",
  "description": "Price alerts workflow",
  "event_types": ["run.completed", "run.failed"],
  "enabled": true,
  "created_at": "2026-09-28T08:14:03Z",
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw"
}

The API returns secret only in this response. Store it with your other secrets. An organization admin can reveal it again on the Connections page.

  • url must use HTTPS and a public host name. IP addresses, credentials in the URL, and local names such as localhost or hosts ending in .local or .internal are refused with 400 Bad Request.
  • event_types lists the events to send. Omit it to receive every event type, including types added later. An empty list is refused. webhook.test cannot be subscribed to; it is sent only on request.
  • description is optional, up to 200 characters.
  • An organization can have up to 100 webhooks. Creating one more returns 409 Conflict. Contact us if you need more.
  • Creating a webhook requires an Idempotency-Key header and an active subscription, like other create operations.

List, read, and delete webhooks

# List the organization's webhooks
curl -s "https://api.extralt.com/v1/webhooks" \
  -H "Authorization: Bearer $EXTRALT_API_KEY"

# Read one webhook
curl -s "https://api.extralt.com/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $EXTRALT_API_KEY"

# Delete a webhook
curl -s -X DELETE "https://api.extralt.com/v1/webhooks/$WEBHOOK_ID" \
  -H "Authorization: Bearer $EXTRALT_API_KEY"

The list response is { "data": [...] } with every webhook, without paging. Webhooks returned by these routes do not include secret. event_types is null for a webhook that receives every event type.

Deleting returns 204 No Content and removes the webhook's delivery history. Events still waiting for a retry are not sent. Deleting works during the grace period after a subscription ends, so you can switch automations off.

The API has no update route. Edit, enable, or disable a webhook on the Connections page, or delete it and create a new one.

Send a test event

curl -s -X POST "https://api.extralt.com/v1/webhooks/$WEBHOOK_ID/test" \
  -H "Authorization: Bearer $EXTRALT_API_KEY"

The response is 202 Accepted with the event's id, { "event_id": "evt_…" }. Extralt then sends a webhook.test event to the endpoint, whatever event types it subscribes to:

{
  "id": "evt_jh71x4q9c2v8m3z6t5r0n9wbf2p7akdq",
  "type": "webhook.test",
  "created_at": "2026-10-13T09:00:00Z",
  "data": {
    "webhook_id": "pn74h2q9x8c3v1m6z5t0r7wbk2f8djsa",
    "message": "This is a test event from Extralt."
  }
}

A test event is signed like any other event but sent only once, without retries. A disabled webhook returns 409 Conflict.

Receiving events

Each event is a POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
User-AgentExtralt-Webhooks/1.0
webhook-idThe event's id, such as evt_…
webhook-timestampWhen this attempt was sent, in Unix seconds
webhook-signaturev1, followed by the base64 signature

The body is an envelope around the resource:

{
  "id": "evt_kq83n2x7c4v9m1z6t5r0p8wbd3f2hjsa",
  "type": "run.completed",
  "created_at": "2026-09-28T09:02:11Z",
  "data": {
    "id": "kh72p9xq4c8v1m6z3t5r0n8wbd2f4hje",
    "robot_id": "jd7c4hq2zr8m1v6x9t3k5n0wbe2f7ays",
    "robot_name": "example-store_com_US",
    "name": "example-store_com_US_4",
    "status": "completed",
    "finished": true,
    "triggered_by": "schedule",
    "…": "the rest of the Run object"
  }
}
  • id identifies the event for this endpoint. It is the same on every retry and redelivery, so use it to deduplicate. Two webhooks that receive the same event get different ids.
  • type is one of the event types.
  • created_at is when the event occurred, an RFC 3339 timestamp in seconds.
  • data is rendered once, at the first attempt, and every retry sends the same bytes. It reflects the resource at that moment. Read the resource from the API when you need its current state.

Respond quickly

Return any 2xx status within 10 seconds to acknowledge the event. Any other status, a timeout, or a connection error counts as a failure and is retried. Redirects are not followed, so a 3xx response is a failure too.

Acknowledge first and do slow work afterward, for example by putting the event on a queue. The first 1,000 characters of your response body are kept and shown with the delivery on the Connections page, which helps when debugging.

Delivery guarantees

Delivery is at least once and in no guaranteed order:

  • The same event can arrive more than once. Deduplicate on id or the webhook-id header.
  • Events can arrive out of order, for example a retried run.failed after the run.completed of the same restarted Run. Use created_at, or read the resource from the API, to decide which state is current.

Verifying signatures

Verify every event before acting on it. The signature is an HMAC-SHA256, keyed with the base64-decoded part of the secret after whsec_, over the string {webhook-id}.{webhook-timestamp}.{body}. Verify against the raw request body, before parsing it as JSON, since re-serializing changes the bytes.

The Standard Webhooks libraries do this for you. They also reject a timestamp more than five minutes from your clock, which protects against replayed requests.

# pip install standardwebhooks
import os

from standardwebhooks.webhooks import Webhook, WebhookVerificationError

webhook = Webhook(os.environ["EXTRALT_WEBHOOK_SECRET"])  # whsec_…


def handle(raw_body: bytes, headers: dict[str, str]):
    try:
        event = webhook.verify(raw_body, headers)
    except WebhookVerificationError:
        return 400  # reject: not from Extralt, or too old
    if event["type"] == "run.completed":
        ...  # event["data"] is the Run
    return 204

Rotating the secret

An organization admin can rotate a webhook's secret from the Connections page, for example when it may have leaked. The new secret is shown right away, and you choose how long the old one keeps working: 24 hours, 7 days, 1 hour, or not at all.

Until the old secret stops, every request carries two signatures in webhook-signature, separated by a space: one with the new secret and one with the old. The Standard Webhooks libraries and the example above accept a request when any signature matches, so you can switch your stored secret at any point in that window without missing an event. An admin can also stop the old secret early from the same menu.

Webhooks that n8n, Zapier or Make created hold their own copy of the secret, and rotating it on the Connections page breaks them once the old one stops. To give such a webhook a new secret, turn the workflow, Zap or scenario off and on again, which deletes the webhook and creates a new one.

Retries

A failed attempt is retried after an increasing wait: 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and 10 hours. That is eight attempts over about 28 hours. After the last one, the delivery is marked failed. You can redeliver it from the Connections page for 30 days, after which delivery history is deleted.

An event whose resource was deleted before the first attempt is not sent.

Disabled webhooks

A webhook whose attempts have all failed for five days is disabled, and the Connections page shows why. Its enabled field becomes false in the API. While a webhook is disabled, new events are not queued for it and pending retries are dropped. Disabling a webhook yourself on the Connections page does the same.

A webhook disabled for failing is deleted, with its delivery history, if it is not enabled again within 30 days.

Removing a webhook from your side

Answer an event with 410 Gone to say the webhook will never accept events again. Extralt deletes the webhook right away and does not retry. Workflow tools such as Zapier answer 410 once their subscription is deleted, so a webhook left behind by a deleted automation does not count against the limit.

To recover, fix the endpoint, enable the webhook on the Connections page, and send a test event. Events from while it was disabled are not sent afterwards, so list the resources that changed in the meantime from the API, for example Runs, and reconcile.

Next steps