Webhooks push platform events to an endpoint you control. Every delivery is signed, and every delivery may arrive more than once.
Subscribing
POST /v1/webhooks
{
"url": "https://example.com/hooks/cymbiote",
"events": ["post.published", "post.updated", "media.processed"],
"description": "Production cache invalidation"
}
The response includes a secret. It is shown once. Store it somewhere you can rotate.
Verifying signatures
Every request carries X-Cymbiote-Signature and X-Cymbiote-Timestamp. Verify before doing anything else:
import crypto from "node:crypto";
export function verify(rawBody: string, signature: string, timestamp: string, secret: string) {
// Reject anything older than five minutes — blocks replay.
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time compare; `===` leaks timing information.
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Verify against the raw body. If your framework has already parsed and re-serialised the JSON, the bytes will differ and every signature will fail.
Delivery semantics
Delivery is at-least-once. Network timeouts and 5xx responses are retried with exponential backoff over 24 hours: after 1 minute, 5, 30, 2 hours, 6 hours and 24 hours.
Responding
Return 2xx within 5 seconds. Do the actual work asynchronously — a handler that returns after 30 seconds of processing is a handler that will be retried while it is still working.
Consistent failures for 72 hours disable the subscription and email the project owners.