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.created or vote.created?
  • Why does my handler reject Canny retries or replays after a few minutes?
  • What are the canny-timestamp, canny-nonce and canny-signature headers?

Verification (core)

HeaderValue
webhook-idUnique message ID
webhook-timestampUnix seconds, covered by the signature
webhook-signatureOne 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

EventFired when
post.created / post.edited / post.deletedA post is created, edited or deleted
post.status_changedA post's status changes
post.tag_added / post.tag_removedA post is tagged / a tag is removed
post.jira_issue_linked / post.jira_issue_unlinkedA Jira issue is linked to / unlinked from a post
comment.created / comment.edited / comment.deletedA portal comment is created, edited or deleted
vote.created / vote.deletedA 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 6, 2026

View on GitHub →