Agent skill

GC Notify Webhooks Skill

Receive and authenticate GC Notify webhooks (Government of Canada notification service callbacks). Use when setting up a GC Notify callback endpoint, checking the Authorization: Bearer token, debugging 401s, or handling email and SMS delivery receipts with statuses like delivered, permanent-failure, temporary-failure and technical-failure.

Install this skill

npx skills add hookdeck/webhook-skills --skill gc-notify-webhooks


GC Notify is the Government of Canada's notification service (a fork of GOV.UK Notify, but a separate service). When an email or text message you sent is delivered or fails, GC Notify POSTs a JSON delivery receipt to the callback URL you registered. Callbacks are not signed. You set a bearer token for the callback, and GC Notify sends it on every request as Authorization: Bearer <token>. Your endpoint authenticates the request by comparing it with the token you stored.

When to Use This Skill

  • How do I receive GC Notify callbacks (webhooks)?
  • How do I verify the Authorization: Bearer token on a GC Notify callback?
  • Does GC Notify sign webhook payloads? (No: it sends a static bearer token)
  • How do I handle delivered, permanent-failure or technical-failure delivery receipts?
  • Why does my GC Notify callback endpoint return 401?

Verification (core)

const crypto = require('crypto');

const BEARER = /^bearer /i; // auth schemes are case-insensitive (RFC 9110)

function safeEqual(a, b) { // Buffers; hash both sides so lengths match
  const ha = crypto.createHash('sha256').update(a).digest();
  const hb = crypto.createHash('sha256').update(b).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyGcNotifyRequest(headers, configured = process.env.GC_NOTIFY_WEBHOOK_SECRET) {
  const secret = configured ? configured.replace(BEARER, '') : ''; // bare token; never trim
  if (!secret) return false;                      // never accept when unconfigured
  const header = headers['authorization'];
  if (typeof header !== 'string' || !BEARER.test(header)) return false;
  return safeEqual(Buffer.from(header.slice(7), 'latin1'), Buffer.from(secret, 'utf8'));
}
  • Header: Authorization: Bearer <token>. Store just the token in GC_NOTIFY_WEBHOOK_SECRET.
  • Return 401 for a missing header, a scheme other than Bearer, or a mismatch.
  • Read the body raw (express.raw({ type: '*/*' })) so any content type works, and parse it only after the check.
  • Limits of this check: a static shared credential authenticates the sender, not the body, and gives no replay protection (there is no signature and no timestamp). Use HTTPS and rotate the bearer token in GC Notify if it leaks.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Hookdeck Setup Note

When you create the GC Notify source in Hookdeck (source type GC_NOTIFY, labelled "GC Notify"), it shows this note: "Paste the bearer token you set for the callback in GC Notify." Paste only the token; Hookdeck matches the Bearer scheme itself.

Delivery Receipt Statuses

Callbacks have no event field. Every body is a delivery receipt; dispatch on status. All values are strings.

The docs list 10 statuses but only 9 descriptions, so they don't pair one-to-one; use status, not status_description. Matched by meaning and notification-api's labels:

statusMeaning (status_description)
created, sending, pendingIn transit
sentNot paired in the docs; notification-api labels it Delivered for email, Sent for SMS
deliveredDelivered
permanent-failureBlocked, No such number, or No such address
temporary-failureContent or inbox issue, or Carrier issue
technical-failureTech issue (provider_response has detail)
pending-virus-checkIn transit
virus-scan-failedAttachment has virus

Other fields: id, reference, to, status_description, provider_response, created_at, completed_at, sent_at and notification_type (email or sms). A notification can produce several callbacks (for example delivered, then a bounce), so handle them idempotently.

GC Notify also POSTs {"health_check": "true"} when you set up or update callbacks; the examples acknowledge it with 200.

Environment Variables

GC_NOTIFY_WEBHOOK_SECRET=your-callback-bearer-token   # the token you set for the callback

Delivery Behaviour

Respond within 1 second (GC Notify's guidance). A failed callback is retried 6 times over about 25 minutes; after repeated errors GC Notify suspends callbacks for your service and emails you, and you must contact them to resume.

Local Development

GC Notify can't reach localhost. Tunnel with the Hookdeck CLI (no account required):

npx hookdeck-cli listen 3000 gc-notify --path /webhooks/gc-notify

Use the printed URL as the callback URL in GC Notify.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →