Guide to GC Notify Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Sign in to GC Notify, then API integration > Callbacks |
| Method and format | POST, JSON body (Content-Type: application/json) |
| Authentication | Authorization: Bearer <token>, with the token you set |
| Signature and timestamp | None |
| Callback content | Email and text message delivery receipts. All values are strings |
| Event type | None. status describes the message |
| Health checks | {"health_check": "true"} on set-up, on update, and when you test response time |
| Response time | GC Notify asks you to respond within 1 second |
| Retries | 6 times over 25 minutes, then an email to your team |
| Suspension | After repeated errors, callbacks stop until you ask GC Notify to resume them |
| Ordering | Callbacks are sent in the order they are received |
| Source IPs | Not 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:
| Key | Description |
|---|---|
id | The notification's ID in GC Notify (UUID) |
reference | The reference your service sent with the notification |
to | The recipient's email address or phone number |
status | created, sending, pending, sent, delivered, permanent-failure, temporary-failure, technical-failure, pending-virus-check or virus-scan-failed |
status_description | An English label, such as In transit, Delivered, No such address, Carrier issue, Tech issue or Attachment has virus |
provider_response | Detail from the provider, only set for a technical failure |
created_at | When your service sent the request |
completed_at | The last time the status was updated, or nil |
sent_at | When the notification was sent, or nil |
notification_type | email 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.
- Sign in to GC Notify.
- Go to the API integration page and select Callbacks.
- Enter your HTTPS callback URL and the Bearer token.
- 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
200as 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
idplusstatus, and upsert rather than insert. - Use
completed_atto 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_checkfirst, then for the receipt fields, and log anything else. - Route on
statusandnotification_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.