Webhook events
Real-time notification of agent platform events. POSTs are signed with HMAC-SHA256, retried with exponential backoff, and deactivated after persistent failure so a dead consumer doesn't keep eating queue capacity.
Event types
attestation.createdeventattestation.acceptedeventattestation.rejectedeventattestation.expiredeventvalidation.blockedeventkey.revokedeventkey.expiredeventPayload shape
{
"id": "1d4f7c8e-9b2a-4d6f-…", // unique per delivery
"type": "attestation.created",
"createdAt": "2026-06-15T14:22:11.034Z",
"data": {
"serviceRequestId": 4193,
"agentKeyId": "8a0b…",
"action": "buy",
"assetSymbol": "BTC",
"assetType": "crypto",
"amountUsd": 5000,
"rationale": "Add a 1% nudge while crypto sleeve has headroom.",
"ipsVersion": 7
}
}Signature verification
We sign every delivery with the secret shown once at subscription time. The header is X-AC-Signature in the form t=<unix-seconds>,v1=<hex>. The HMAC body is <unix-seconds>.<raw-request-body> — the timestamp is inside the HMAC to defeat replay.
import { createHmac, timingSafeEqual } from 'crypto';
export function verify(rawBody: string, header: string, secret: string) {
const parts = Object.fromEntries(
header.split(',').map((p) => p.split('=') as [string, string]),
);
const ts = Number(parts.t);
const sig = parts.v1;
if (!ts || !sig) return false;
// Reject deliveries older than 5 minutes.
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
if (expected.length !== sig.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Request headers we send
Content-TypeheaderUser-AgentheaderX-AC-SignatureheaderX-AC-EventheaderX-AC-Delivery-IdheaderRetries
- A delivery is considered successful only if your endpoint returns a 2xx within 10 seconds.
- On failure we retry up to 6 times with exponential backoff starting at 5 seconds.
- After 20 consecutive failures on the same webhook we flip
isActivetofalse. You re-enable it manually after fixing the consumer.
Idempotency on your side
We may deliver the same event more than once — a transient 5xx from your endpoint that we retry, a redelivery if your handler responded too slowly. Treat X-AC-Delivery-Id as the dedupe key and store a short-lived “already processed” cache on your side.
Responding
Acknowledge as fast as you can — ideally under one second. Do real work after responding. We don’t care about the response body; status code is all that matters.
app.post('/webhooks/ac', express.raw({ type: '*/*' }), (req, res) => {
if (!verify(req.body.toString(), req.header('X-AC-Signature')!, SECRET)) {
return res.status(401).end();
}
res.status(204).end(); // ack fast
enqueueProcessing(req.body); // do work async
});