Webhooks
Subscribe to account events and verify signed deliveries.
Register a subscription
POST /api/v1/webhooks with a delivery URL and the event types you care about. The signing secret is returned once in the 201 response — store it. (accountId is set server-side from your token.)
cURL
curl -X POST https://app.canopymethod.ai/api/v1/webhooks \
-H "Authorization: Bearer $CANOPY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/canopy/webhook",
"eventTypes": ["issue.status_changed", "outcome.shipped"]
}'Payload & headers
Deliveries are POSTed to your URL with a JSON body { type, occurredAt, data } and these headers:
X-Canopy-Signature: t=<ts>,v1=<hmac-sha256>(Stripe-style)X-Canopy-Event-Id: <uuid>— for de-duplication
Verifying the signature
Recompute the HMAC-SHA256 of `${t}.${rawBody}` using your stored secret and compare it to v1 with a constant-time check. Reject if it does not match.
TypeScript (Node)
import crypto from "node:crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
}Python (Flask)
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
expected = hmac.new(
secret.encode(),
f"{parts['t']}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Test-fire a delivery
Send a signed test event to your subscription to confirm your endpoint and verification logic:
curl -X POST https://app.canopymethod.ai/api/v1/webhooks/{id}/test \
-H "Authorization: Bearer $CANOPY_TOKEN"Always verify before trusting a payload. Reject any delivery whose
X-Canopy-Signature does not match, and de-duplicate on X-Canopy-Event-Id since deliveries may retry.