Guide to GoCardless Webhooks: Features and Best Practices
GoCardless webhooks notify your application about bank debit activity: a mandate is created or cancelled, a payment is confirmed or fails, a payout is paid. If you're building on GoCardless for bank debit or open banking payments, webhooks are how you react to these events without polling.
This guide covers how GoCardless webhooks work, the event batches you'll handle, how to verify the Webhook-Signature, and the best practices for production.
What are GoCardless webhooks?
GoCardless webhooks are JSON POSTs delivered to a URL you configure. A single POST carries an array of events (events: [...]) with up to 250 events per request. Each is signed with a Webhook-Signature header (no X- prefix, no scheme prefix in the value), computed over the raw POST body with your webhook endpoint's secret. GoCardless doesn't name the algorithm in its own prose, but it's HMAC-SHA256: the value is a 64-character lowercase hex digest, which both GoCardless's official Node SDK computes that way and reproduces exactly when you verify a real delivery. Use the endpoint secret verbatim as the HMAC key, don't base64-decode it. The official Node SDK also verifies for you.
GoCardless webhook features
| Feature | Details |
|---|---|
| Configuration | Developers > Webhook endpoints |
| Signature header | Webhook-Signature (no prefix) |
| Signature scheme | HMAC-SHA256 (hex) over the raw body, keyed with the endpoint secret used verbatim; the SDK computes it |
| Envelope | An array events: [...], up to 250 events per POST |
| Event fields | resource_type and action are separate fields (not a dotted name) |
| Retries | No automatic schedule documented; manual POST /webhooks/{id}/actions/retry |
| Invalid signature | Docs recommend replying 498 |
| SDK | npm gocardless-nodejs (verifies); pip gocardless_pro |
Common events
Each element carries resource_type and action as separate fields (there's no single dotted payments.confirmed name). Confirmed pairs:
| resource_type / action | Fires when |
|---|---|
mandates / created, cancelled, customer_approval_granted | A mandate is created, cancelled, or approved |
payments / created, submitted, confirmed, paid_out | A payment progresses |
payments / failed, charged_back, cancelled, resubmission_requested | A payment has trouble |
payouts / paid | A payout is paid |
Each element also carries details{origin, cause, description, scheme, reason_code} and links{}. Dispatch on the resource_type + action combination.
Setting up GoCardless webhooks
In the dashboard, go to Developers > Webhook endpoints, add your HTTPS URL, and copy the endpoint secret into GOCARDLESS_WEBHOOK_SECRET. Use the durable docs at docs.gocardless.com (the legacy developer.gocardless.com is deprecating on 24 Aug 2026).
Securing GoCardless webhooks
In Node, use the official gocardless-nodejs/webhooks parse(), which verifies the Webhook-Signature over the raw body and returns the events array (throwing InvalidSignatureError on a mismatch). In other languages, compute a keyed hash over the raw body with your secret and compare in constant time. Verify against the raw body, re-serializing changes the bytes. The docs recommend replying 498 to an invalid signature.
const { parse, InvalidSignatureError } = require("gocardless-nodejs/webhooks");
const SECRET = process.env.GOCARDLESS_WEBHOOK_SECRET;
app.post("/webhooks/gocardless", express.raw({ type: "application/json" }), (req, res) => {
let events;
try {
events = parse(req.body, SECRET, req.headers["webhook-signature"]); // raw body
} catch (err) {
if (err instanceof InvalidSignatureError) return res.sendStatus(498); // recommended
return res.sendStatus(400);
}
res.sendStatus(204); // acknowledge the whole batch
for (const event of events) {
processQueue.add(event); // dedupe on event.id, dispatch on resource_type + action
}
});
The same verification in Python (manual HMAC-SHA256 over the raw body). Use the endpoint secret verbatim as the key:
import hmac
import hashlib
import os
SECRET = os.environ["GOCARDLESS_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, signature_header: str) -> bool:
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
# then json.loads(raw_body)["events"] is an array of up to 250 events
GoCardless webhook limitations and pain points
The body is an array of up to 250 events
The Problem: A single POST carries events: [...] with up to 250 events. A handler written for a single-event body silently drops the other 249.
Why It Happens: GoCardless batches events into one delivery.
Workarounds:
- Iterate the
eventsarray and process each element, then acknowledge once.
How Hookdeck Can Help: Hookdeck can fan a batch out into individual events, so each is delivered, retried, and observed on its own.
The algorithm isn't named in the docs
The Problem: GoCardless never names the signing algorithm in its prose, only that the signature is computed from the body and the endpoint secret. From the docs alone it isn't obvious which scheme to implement.
Why It Happens: The docs describe the signature by its inputs, not by name.
Workarounds:
- It's HMAC-SHA256, hex, over the raw body, with the endpoint secret used verbatim as the key (confirmed both by GoCardless's official SDK and by reproducing the
Webhook-Signatureon a live delivery). Use the official Node SDK, or that scheme directly.
How Hookdeck Can Help: Hookdeck verifies GoCardless deliveries at the edge, so you don't have to reconstruct the scheme from the docs.
There's no automatic retry
The Problem: GoCardless documents no automatic retry schedule. If your endpoint is down, events aren't retried on a timer, you retry them manually.
Why It Happens: GoCardless exposes a manual retry endpoint (POST /webhooks/{id}/actions/retry) rather than an automatic backoff.
Workarounds:
- Acknowledge reliably, and reconcile missed events via the API or the manual retry endpoint.
How Hookdeck Can Help: Hookdeck durably queues events and retries on its own schedule, so a downstream outage doesn't require manual recovery.
Separate fields, and the 498 response
The Problem: Events identify themselves with separate resource_type and action fields, not a dotted name, and the docs recommend the non-standard 498 status for an invalid signature. Handlers keyed to dotted names, or returning a standard 4xx, get it wrong.
Why It Happens: GoCardless models events as a resource plus an action and adopted 498 for signature failures.
Workarounds:
- Dispatch on
resource_type+action, and reply498on a bad signature.
How Hookdeck Can Help: Hookdeck's filters route on the fields you receive, so your app isn't coupled to a naming convention.
Best practices
Iterate the events array
Loop over events (up to 250) and process each; acknowledge the batch once.
Verify with the official SDK
Use gocardless-nodejs/webhooks parse() in Node; treat a manual HMAC-SHA256 as inferred and confirm it against a real delivery.
Dedupe on event.id and reply 498 on failure
Event creation is asynchronous, so dedupe on event.id; reply 498 to an invalid signature.
Acknowledge fast, process asynchronously
Return 2xx (204) quickly and defer work to a queue. See why to process webhooks asynchronously.
Make GoCardless webhooks production-ready
Hookdeck verifies Webhook-Signature, fans out batches, deduplicates, and durably queues every event
Conclusion
GoCardless delivers webhooks as an array of up to 250 events, verified with a Webhook-Signature HMAC-SHA256 (hex) over the raw body, keyed with the endpoint secret used verbatim (the Node SDK computes it). Iterate the array, dispatch on the separate resource_type and action fields, dedupe on event.id, reply 498 on a bad signature, and reconcile via the API since there's no automatic retry.
Hookdeck verifies the signature, fans out batches, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique events.
Get started with Hookdeck for free and handle GoCardless webhooks reliably in minutes.