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 docs; the signature values are 64-character lowercase hex, consistent with HMAC-SHA256, so treat that as an informed inference rather than a documented fact. The official Node SDK verifies for you, which sidesteps the ambiguity.
GoCardless webhook features
| Feature | Details |
|---|---|
| Configuration | Developers > Webhook endpoints |
| Signature header | Webhook-Signature (no prefix) |
| Signature scheme | Keyed hash over the raw body (inferred HMAC-SHA256, hex); 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, keyed hash over the raw body). GoCardless doesn't name the algorithm; the observed 64-hex values match HMAC-SHA256:
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 documented
The Problem: GoCardless never names the signing algorithm in prose. The values look like HMAC-SHA256, but that's an inference, so a from-scratch implementation is built on an assumption.
Why It Happens: The docs describe the signature only as computed from the body and the endpoint secret.
Workarounds:
- Use the official Node SDK, which computes the correct scheme; treat any manual HMAC-SHA256 as inferred and confirm against a real delivery.
How Hookdeck Can Help: Hookdeck verifies GoCardless deliveries at the edge, so you don't depend on an inferred algorithm.
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 keyed hash over the raw body (an inferred HMAC-SHA256; 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.