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.