Agent Credential Webhook Events, and Why the Timestamp Is Not Where You Will Look For It
What you get
Subscribe an endpoint and a list of events, and the subscribe response returns a signing secret. That secret appears in that one response and never again.
Deliveries arrive as signed requests, retried with exponential backoff when they fail.
The signature, and the part that trips people
Each delivery carries an HMAC over the request body, keyed by your secret, in a signature header with the algorithm named in the value.
The headers carry the event name and a delivery identifier, and no timestamp. Webhook conventions usually put a timestamp in a header and sign it alongside the body, so a receiver who knows the pattern will look at the headers, see no timestamp, and conclude there is nothing to check freshness against.
There is. It is in the body, and the body is what gets signed. Each delivery's JSON carries its own identifier, its event name, and the time it was created.
So replay defence is available and it is entirely your job:
- Verify the signature over the raw body, before parsing it, with a constant-time comparison.
- Read the creation time from inside the body and reject deliveries older than a bound you choose.
- Store the delivery identifier and refuse a repeat, because retries mean at-least-once and a captured delivery stays validly signed.
Nothing in the protocol forces any of the three, which is why they are the first section rather than a footnote.
The outbound request is hardened better than most
Private, loopback, link-local and cloud-metadata address ranges are refused, and redirects are not followed.
And the check runs again at delivery time, with the address pinned. An endpoint that passed validation when you subscribed and whose name later resolves to an internal address is refused at delivery.
That closes the gap between checking and connecting, which is the one most implementations leave open. It deserves saying plainly, because it is the sort of care that never shows up in a feature list.
Every payload is a whitelist
An agent registration carries the agent's identifier, its decentralised identifier and its custody mode. A credential issuance or revocation carries the credential identifier, the agent identifier and the credential type.
That is the whole payload. No claims, no assurance level, no operator identity, nothing about a subject.
The consequence is the point: a new column added to a table does not appear in your webhook next week. Anything that leaves the building is enumerated rather than filtered, which is the only construction that stays safe as a schema grows.
This page covers agent and credential events and does not claim to list every event the product emits.
Delivery semantics, stated so you can build against them
Idempotent on our side. A delivery already marked delivered is not sent again by a repeat job.
At-least-once from your side. Retries exist, so plan for duplicates rather than assuming they cannot happen.
Attempt counts are honest. A failed attempt is recorded as failed rather than being smoothed into a success, which matters the first time you debug a missing event.
What we have not done
No timestamp header, which is a convention gap rather than a security gap, and the fix is either a header or documentation. This page is currently the documentation.
No published receiver example. The three steps above are a specification, not a tested snippet, and this page does not pretend otherwise.
Keep reading
- How a Relying Party Verifies an Agent Credential, and Why `valid` Is Not the Field You Want
- How Revocation Works, and the One Case Where the Authoritative Answer Is the Stale One
- Agent Identity SDK Quickstart, With the Three Things a Quickstart Usually Hides
- Credential-Gated API Access, and the Line Between What the Proof Covers and What It Does Not

