Agent skill
Canny Webhooks Skill
Receive and verify Canny (canny.io) product feedback webhooks, signed with the Standard Webhooks scheme. Use when setting up a Canny webhook endpoint, finding the whsec_ signing secret, debugging webhook-id / webhook-timestamp / webhook-signature verification, handling late Hookdeck retries rejected by the 5-minute window, or handling post.created, post.status_changed, comment.created and vote.created events.
Install this skill
npx skills add hookdeck/webhook-skills --skill canny-webhooks
Canny collects product feedback: boards, posts, comments and votes. Its webhooks notify your server as those things happen, and every request is signed following the Standard Webhooks specification.
When to Use This Skill
- How do I receive Canny webhooks?
- How do I verify a Canny webhook signature (
webhook-signature)? - Where do I find my Canny webhook signing secret?
- How do I handle
post.created,post.status_changed,comment.createdorvote.created? - Why does my handler reject Canny retries or replays after a few minutes?
- What are the
canny-timestamp,canny-nonceandcanny-signatureheaders?
Verification (core)
| Header | Value |
|---|---|
webhook-id | Unique message ID |
webhook-timestamp | Unix seconds, covered by the signature |
webhook-signature | One or more space-separated v1,<base64> entries (two for 24 hours after you regenerate the secret) |
Signed content is {webhook-id}.{webhook-timestamp}.{raw body}, HMAC-SHA256, base64. The key is the secret without whsec_, base64-decoded. Accept the request if any v1, entry matches. Canny's docs recommend an official Standard Webhooks library:
const { Webhook } = require('standardwebhooks');
// rawBody: exact request body string, NOT re-serialised JSON
function verifyCannyWebhook(rawBody, headers, secret = process.env.CANNY_WEBHOOK_SECRET) {
return new Webhook(secret).verify(rawBody, {
'webhook-id': headers['webhook-id'],
'webhook-timestamp': headers['webhook-timestamp'],
'webhook-signature': headers['webhook-signature'],
}); // returns the parsed event; throws -> respond 400
}
Python: from standardwebhooks.webhooks import Webhook, then Webhook(secret).verify(await request.body(), {...same three headers...}).
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
The 5-minute window and Hookdeck retries
The standardwebhooks and svix libraries always reject a webhook-timestamp more than 5 minutes from your clock, and the window can't be turned off. Hookdeck retries and replays resend the original request, including its original webhook-timestamp. A delivery retried or replayed more than 5 minutes after Canny sent it fails the library check even though the signature is genuine. If your handler sits behind Hookdeck retries, use the manual verifier in references/verification.md: it checks the same HMAC over {id}.{timestamp}.{raw body} and compares every v1, entry in constant time, and lets you drop or widen the window.
Legacy canny-* headers
Teams that used webhooks before signing secrets existed also receive canny-timestamp, canny-nonce and canny-signature. They are legacy and being removed. That signature covers only the nonce (signed with your API key), not the body or timestamp. Ignore them and verify webhook-signature.
Event Envelope
{ "created": "2017-07-15T22:11:00.000Z",
"objectType": "post",
"type": "post.created",
"object": { "id": "6a2889c586d7b8843bf4cf05", "title": "An awesome feature request", "status": "in progress", "board": { "name": "Feature Requests" } } }
Dispatch on the body's type. There is no event ID in the body, so dedupe on the webhook-id header.
Common Event Types
| Event | Fired when |
|---|---|
post.created / post.edited / post.deleted | A post is created, edited or deleted |
post.status_changed | A post's status changes |
post.tag_added / post.tag_removed | A post is tagged / a tag is removed |
post.jira_issue_linked / post.jira_issue_unlinked | A Jira issue is linked to / unlinked from a post |
comment.created / comment.edited / comment.deleted | A portal comment is created, edited or deleted |
vote.created / vote.deleted | A user votes / unvotes on a post |
Full payloads: references/overview.md and Canny's webhook docs.
Environment Variables
CANNY_WEBHOOK_SECRET=whsec_xxxxx # signing secret from API settings; handlers fail closed (500) without it
Add a webhook in Your Subdomain > Settings > API. The signing secret appears on that page once a webhook exists.
Local Development
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 canny --path /webhooks/canny
Add the printed URL as a webhook in Canny's API settings, then create a post or vote in your Canny board.
Reference Materials
- references/overview.md - Event catalog, payload envelope
- references/setup.md - Adding a webhook, signing secret, rotation
- references/verification.md - Library and manual verification (configurable window), gotchas, debugging