Gareth Wilson Gareth Wilson

Guide to GC Notify Webhooks: Features and Best Practices

Published


GC Notify callbacks tell your service what happened to the emails and text messages it sent. GC Notify is the Government of Canada's notification service, and when a message is delivered, bounces or fails, it can post a delivery receipt to a URL you choose, so your case management or records system can track each message without polling the API.

This guide covers how GC Notify callbacks are set up and authenticated, what a delivery receipt contains, how health checks, retries and suspensions work, and the practices that keep a callback endpoint reliable.

What are GC Notify callbacks?

GC Notify calls its webhooks callbacks. For each service, you provide a URL and a Bearer token, and GC Notify sends a POST with a JSON body to that URL whenever the status of an email or text message changes. The token goes in the Authorization header as Bearer <token>.

GC Notify does not sign callbacks. The Bearer token is a static credential that your endpoint compares with the one you set. GC Notify was built from GOV.UK Notify, but it is a separate service with its own documentation, and its callbacks are configured separately.

GC Notify callback features

FeatureDetails
ConfigurationSign in to GC Notify, then API integration > Callbacks
Method and formatPOST, JSON body (Content-Type: application/json)
AuthenticationAuthorization: Bearer <token>, with the token you set
Signature and timestampNone
Callback contentEmail and text message delivery receipts. All values are strings
Event typeNone. status describes the message
Health checks{"health_check": "true"} on set-up, on update, and when you test response time
Response timeGC Notify asks you to respond within 1 second
Retries6 times over 25 minutes, then an email to your team
SuspensionAfter repeated errors, callbacks stop until you ask GC Notify to resume them
OrderingCallbacks are sent in the order they are received
Source IPsNot published

Delivery receipts

Every callback documented on the Callbacks page is a delivery receipt for one notification. GC Notify describes the fields in a table rather than a JSON example:

KeyDescription
idThe notification's ID in GC Notify (UUID)
referenceThe reference your service sent with the notification
toThe recipient's email address or phone number
statuscreated, sending, pending, sent, delivered, permanent-failure, temporary-failure, technical-failure, pending-virus-check or virus-scan-failed
status_descriptionAn English label, such as In transit, Delivered, No such address, Carrier issue, Tech issue or Attachment has virus
provider_responseDetail from the provider, only set for a technical failure
created_atWhen your service sent the request
completed_atThe last time the status was updated, or nil
sent_atWhen the notification was sent, or nil
notification_typeemail or sms

There is no field that names an event. Route on status and notification_type, and match receipts to your own records by id or reference. Status values and descriptions are only in English.

One notification can produce more than one receipt. GC Notify's example is an email that the receiving mail server first accepts, which produces a delivery callback, and then bounces, which produces another.

GC Notify's open-source sending code also contains a task for complaint callbacks, which the Callbacks page does not describe. Write your handler so a body that is not a delivery receipt is logged and acknowledged, not treated as an error.

Inspect GC Notify callbacks as they arrive. Point your callback URL at a Hookdeck Inspect URL and send a message with a test API key to see the receipt's headers and body, with no account or setup required.

Setting up GC Notify callbacks

Generate a long random token first, for example with openssl rand -hex 32.

  1. Sign in to GC Notify.
  2. Go to the API integration page and select Callbacks.
  3. Enter your HTTPS callback URL and the Bearer token.
  4. Save. GC Notify sends a health check request to the URL.

Callbacks work with test API keys, so you can try the set-up without sending real messages. They do not work with GC Notify's smoke-testing email addresses and phone numbers.

GC Notify's guidance on the token is to keep it secure, change it if you have any reason to doubt it, check that every callback carries it, and give GC Notify a hashed value so that GC Notify does not hold your true token. Whatever value you enter is the value GC Notify sends after Bearer .

To route through Hookdeck, create a Hookdeck source with the GC Notify type and use its URL as the callback URL. The source has one field, Bearer Token: paste the bearer token you set for the callback in GC Notify. Paste only the token. GC Notify sends Bearer <token> in the Authorization header, and Hookdeck matches the Bearer scheme itself. The source accepts POST only, which is the method GC Notify uses.

GC Notify's documentation does not say whether the health check request carries the token. Hookdeck answers it with a 200 either way, so set-up succeeds, but if the health check arrives without the token it shows in Hookdeck as a request that failed verification.

Securing GC Notify callbacks

Read the Authorization header, check that the scheme is Bearer (case-insensitively), and compare the rest with your stored token in constant time. Reject the request if either is missing.

const crypto = require('crypto');
const express = require('express');

const CALLBACK_TOKEN = process.env.GC_NOTIFY_CALLBACK_TOKEN; // the token only, no "Bearer "

function verifyBearer(header) {
  if (!CALLBACK_TOKEN || typeof header !== 'string') return false;
  const match = /^bearer (.+)$/i.exec(header);
  if (!match) return false;
  const received = Buffer.from(match[1], 'utf8');
  const expected = Buffer.from(CALLBACK_TOKEN, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/gc-notify', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBearer(req.get('authorization'))) {
    return res.sendStatus(401);
  }

  let receipt;
  try {
    receipt = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  if (receipt.health_check === 'true') return res.sendStatus(200);

  // Store the receipt keyed on receipt.id and receipt.status, then process it asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

CALLBACK_TOKEN = os.environ.get("GC_NOTIFY_CALLBACK_TOKEN", "")  # the token only


def verify_bearer(header) -> bool:
    if not CALLBACK_TOKEN or not header:
        return False
    scheme, _, token = header.partition(" ")
    if scheme.lower() != "bearer":
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(token.encode("utf-8"), CALLBACK_TOKEN.encode("utf-8"))

The token is static. It authenticates the sender, not the body, so a modified receipt still passes, and with no timestamp a captured request can be replayed. Serve the endpoint over HTTPS only, use a long random token, and change it in GC Notify and in your environment together if it might have leaked.

Make GC Notify callbacks production-ready. Hookdeck Event Gateway checks the Bearer token, answers GC Notify straight away, and queues every delivery receipt durably before your service sees it.

GC Notify callback limitations and pain points

A one-second budget and a suspension at the end

The Problem: Your service stops receiving callbacks entirely, and has to ask GC Notify to switch them back on.

Why It Happens: GC Notify asks callback endpoints to respond within 1 second. A failing callback is retried 6 times over 25 minutes, after which GC Notify emails your team. If errors keep coming, GC Notify suspends callbacks for the service, and they stay off until you follow its instructions and contact GC Notify to resume them.

Workarounds:

  • Respond with a 200 as soon as the token is checked and the receipt is stored, and do everything else afterwards.
  • Monitor your endpoint's response times. GC Notify offers response time testing from API Integration > Callbacks.
  • Make sure the email address on the service reaches someone who will act on a failure notice.

How Hookdeck Can Help: Hookdeck accepts each callback as soon as it arrives and stores it, so GC Notify sees fast, successful responses even while your own service is down. Hookdeck then delivers to your service with retries you configure.

Several receipts per message

The Problem: A message is marked delivered, and later failed, or the same status is recorded twice.

Why It Happens: A notification can produce more than one receipt as its status changes, and retries can deliver the same receipt again.

Workarounds:

  • Key stored receipts on id plus status, and upsert rather than insert.
  • Use completed_at to tell which receipt is the most recent.

How Hookdeck Can Help: Hookdeck deduplication can drop repeated receipts before they reach your service, based on fields such as id and status.

Nothing names the event

The Problem: Your handler has to guess what kind of request it received.

Why It Happens: Receipts carry no event type, health checks share the same URL, and the sending code has a complaint callback the docs do not describe.

Workarounds:

  • Check for health_check first, then for the receipt fields, and log anything else.
  • Route on status and notification_type.

How Hookdeck Can Help: Hookdeck filters can keep health checks away from your service and route email and text message receipts to different handlers.

Best practices

Store the token, not the header

Keep only the token in your configuration and match the Bearer scheme in code. If your configuration holds Bearer abc and your code compares it with the token after the scheme, every callback fails.

Acknowledge health checks

Return 200 to {"health_check": "true"} without treating it as a receipt. GC Notify uses it to decide whether your endpoint is reachable.

Make handlers idempotent

Store each receipt before processing it, keyed on id and status. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

One second is a tight budget. Verify, store, return 200, and do the work afterwards. See why to process webhooks asynchronously.

Conclusion

GC Notify sends delivery receipts as JSON POST requests with your token in an Authorization: Bearer header and no signature. Check the scheme and compare the token in constant time over HTTPS, acknowledge health checks, key receipts on id and status, and respond within a second, because repeated failures end in a suspension that only GC Notify can lift.

Hookdeck Event Gateway checks the token, answers GC Notify immediately and keeps every receipt until your service has processed it, so an outage on your side does not get your callbacks suspended.

Get started with Hookdeck for free and handle GC Notify callbacks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.