Skip to main content
Webhooks push ICP signals to a URL you control, so you find out within minutes instead of on your next poll.
The feed is still the source of truth. A webhook is a latency convenience on top of List ICP signals, and its cursor remains the way to catch up on anything a push did not reach you with. Build the poller first; add webhooks when the latency matters.

Register an endpoint

secret is returned once, here. It is not readable afterwards from any endpoint. Store it before you close the response. If you lose it, delete the endpoint and register it again.
Your URL must be HTTPS and must resolve to a public address. Private, loopback, link-local and cloud-metadata destinations are refused with a 422 — and checked again at delivery time, because a hostname that resolves publicly today can be repointed tomorrow. Redirects are not followed. Registering does not replay your history: only events detected after the endpoint exists are pushed to it. To backfill, walk the feed with its cursor.

What arrives

event is byte-for-byte the object the feed serves, so the same parser handles both.

Verify the signature

Check it on every request. An unsigned or wrongly-signed request did not come from us.
The MAC covers <t> + . + the raw request body. Sign the bytes you received, not a re-serialization of the parsed JSON — re-encoding can reorder keys and will not match.
Two details that matter: compare with a constant-time function (hmac.compare_digest, not ==), and reject an old timestamp. The timestamp is inside the signed string, so an attacker replaying a captured request cannot rewrite it without breaking the MAC.

Responding

Answer 2xx and we consider it delivered. Anything else is a failure. Answer quickly and do your work afterwards — an attempt times out after 10 seconds. Returning 202 immediately and queueing internally is the usual shape. Be idempotent. Delivery is at-least-once, like the feed: a push that succeeded on your side but failed to answer in time will arrive again. Deduplicate on event.id, which is stable across retries and identical to the id on the feed.

Retries

A failed delivery is retried six times over about 21 hours: after 1 minute, 5 minutes, 25 minutes, 2 hours, 6 hours, then 12 hours. A short outage or a deploy is invisible; an endpoint down overnight still receives everything. After that the delivery is exhausted and stops. Nothing is lost: the event is still on the feed, and the cursor is how you collect it.

When something breaks

Each row carries attempt_count, response_status and a failure_reason from a fixed set: timeout, dns, tls, connection_refused, connection, blocked_destination, redirect, http_error.
Your endpoint’s response body is never stored — only the status code and that reason. The body comes from an address you control, so we do not persist what we fetch from it.
Once your endpoint is fixed, replay what stopped:
Replay re-sends one delivery. It does not re-read the feed, so it cannot recover events that were never queued for this endpoint — anything from before you registered it, or from while it was disabled. Use the feed’s cursor for those.

Pausing

Set is_enabled: false to stop delivery while keeping the endpoint, its secret and its history:
Events detected while an endpoint is disabled are not queued for it. When you re-enable it, the recent window is picked up, but anything older is only available from the feed.
A watch in shadow mode (is_enabled: false on the ICP signal watch) never pushes anything, whatever your endpoints are set to. That is what shadow mode is for: seeing what an ICP would produce before anyone acts on it.