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
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.<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.
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. Returning202 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
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.
Pausing
Setis_enabled: false to stop delivery while keeping the endpoint, its secret and its history:
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.