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 type | Sent when | data |
|---|---|---|
robot.completed | A Robot build completed | Robot (GET /v1/extract/robots/{id}) |
robot.failed | A Robot build failed | Robot |
run.completed | A Run completed | Run (GET /v1/extract/runs/{id}) |
run.stopped | A Run was stopped | Run |
run.failed | A Run failed | Run |
run.enrichment_skipped | A Run with auto_enrich completed, but its automatic Enrichment could not start | Run |
enrichment.completed | An Enrichment completed | Enrichment (GET /v1/enrich/enrichments/{id}) |
enrichment.stopped | An Enrichment was stopped | Enrichment |
enrichment.failed | An Enrichment failed | Enrichment |
enrichment.changes | An Enrichment found price, stock or Listing changes | Enrichment, with its changes counts |
import.completed | An Import completed | Import (GET /v1/extract/imports/{id}) |
import.failed | An Import failed, including failed validation | Import |
webhook.test | You sent a test event | A 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.
urlmust use HTTPS and a public host name. IP addresses, credentials in the URL, and local names such aslocalhostor hosts ending in.localor.internalare refused with400 Bad Request.event_typeslists the events to send. Omit it to receive every event type, including types added later. An empty list is refused.webhook.testcannot be subscribed to; it is sent only on request.descriptionis 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-Keyheader 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:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Extralt-Webhooks/1.0 |
webhook-id | The event's id, such as evt_… |
webhook-timestamp | When this attempt was sent, in Unix seconds |
webhook-signature | v1, 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"
}
}ididentifies 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.typeis one of the event types.created_atis when the event occurred, an RFC 3339 timestamp in seconds.datais 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
idor thewebhook-idheader. - Events can arrive out of order, for example a retried
run.failedafter therun.completedof the same restarted Run. Usecreated_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 204Rotating 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
- Common patterns: polling, retries, and pagination
- API reference: the Webhooks routes and every resource schema
- Standard Webhooks: the signing specification