Gareth Wilson Gareth Wilson

Guide to GoCardless Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationDevelopers > Webhook endpoints
Signature headerWebhook-Signature (no prefix)
Signature schemeKeyed hash over the raw body (inferred HMAC-SHA256, hex); the SDK computes it
EnvelopeAn array events: [...], up to 250 events per POST
Event fieldsresource_type and action are separate fields (not a dotted name)
RetriesNo automatic schedule documented; manual POST /webhooks/{id}/actions/retry
Invalid signatureDocs recommend replying 498
SDKnpm 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 / actionFires when
mandates / created, cancelled, customer_approval_grantedA mandate is created, cancelled, or approved
payments / created, submitted, confirmed, paid_outA payment progresses
payments / failed, charged_back, cancelled, resubmission_requestedA payment has trouble
payouts / paidA 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 events array 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 reply 498 on 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.


Gareth Wilson

Gareth Wilson

Product Marketing

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