GoCardlessDeveloper Docs
Create a sandbox account

Webhooks for partners#

View as Markdown

A webhook is a real-time request that GoCardless sends to your server to alert you of an event.

Adding support for webhooks allows you to receive real-time notifications from GoCardless when events happen in your users' accounts, so you can take automated actions. This is the suggested monitoring approach.

A webhook can contain multiple events, and each has:

  • A resource_type, telling us what kind of resource the event is for, for example, payments or mandates
  • An action, telling us what happened to the resource, for example, the failed action for a payment
  • details, specifying why the event happened
  • links, including links.organisation, the ID of the user's GoCardless account the event belongs to
{
  "events": [
    {
      "id": "EV123",
      "created_at": "2026-04-08T17:01:06.000Z",
      "resource_type": "payments",
      "action": "failed",
      "details": {
        "origin": "bank",
        "cause": "insufficient_funds",
        "description": "The customer's account had insufficient funds to make this payment.",
        "scheme": "sepa_core",
        "reason_code": "AM04",
        "will_attempt_retry": false
      },
      "metadata": {},
      "links": {
        "payment": "PM123",
        "organisation": "OR123"
      }
    }
  ]
}
Stay up to date with webhooks

Read more about how to create a secure webhook handler in the staying up-to-date with webhooks section.

Handling webhooks at scale#

One webhook endpoint receives events for all of your users, and each webhook can contain up to 250 events. To handle them reliably:

  1. Queue incoming webhooks. Respond straight away, then process the events in the background
  2. Verify the signature in the Webhook-Signature header, so you only act on webhooks from GoCardless. See secure your webhooks
  3. Check for duplicates. The same event can arrive more than once, so skip event IDs you've already processed
  4. Filter by event type. Only handle the events your integration needs
  5. Route each event to the right user by matching links.organisation to the organisation ID you stored when they connected their account. Log and discard any events you can't match
  6. Handle events you didn't trigger. You also get events for mandates and payments your users create in the GoCardless dashboard, and for changes we or the banks make, like chargebacks. Don't fail when an event is for a resource you have no record of. Log it, and respond with a 2xx status so we don't retry it

A webhook endpoint queues incoming events. An event router verifies each signature, checks for duplicates, filters by event type and extracts the merchant ID, then sends each event to the right merchant. Events that don't match a merchant go to a bin.

Out-of-order and missed events#

Webhooks can arrive out of order. For example, a payment's confirmed event can arrive before its created event. Before you update a record, fetch the resource's current state from the API, rather than relying on the order events arrive in.

If your endpoint is down, we retry each webhook up to 8 times, at increasing intervals. To catch anything you still missed, list events for each connected user with created_at[gt] set to the time of the last event you processed. Do this after an outage, and on a regular schedule.

Key webhook events#

The descriptions follow the API, which calls the payer the customer.

Billing request webhooks#

EventDescriptionSuggested handling
flow_visitedThe billing request flow has been visitedUse to detect payer inactivity and expire the billing request if needed
flow_exitedThe payer exited the flow without completing itConsider triggering a push notification for mobile experiences
bank_authorisation_deniedA bank authorisation was denied by the payerDisplay the denial reason from details.description
bank_authorisation_failedA bank authorisation failed due to a bank account mismatchDisplay the failure reason from details.description
fulfilledThe billing request was fulfilled and the mandate and/or payment have been createdSave links.mandate_request_mandate and/or links.payment_request_payment against your records

billing-request-state-diagram

See the full list of billing request webhooks in the API reference.

Mandate webhooks#

EventDescriptionSuggested handling
createdThe mandate has been createdSave the mandate ID against your customer record
submittedThe mandate has been submitted to the banksShow the mandate as pending
activeThe mandate has been successfully set up by the customer's bankShow the mandate as active
failedThe mandate could not be set up — the bank account does not accept Direct Debit or is closedShow the reason from details.description, and let your user ask the payer for another bank account or payment method
cancelledThe mandate was cancelled by the customer, via the API, or automatically when their account was closedShow the mandate as cancelled, and let your user ask the payer for a new mandate. We cancel its subscriptions, instalment schedules and pending payments automatically.
expiredNo collections were made within the dormancy period and the mandate has expiredShow the mandate as expired, and let your user reinstate it or ask for a new one. We cancel its subscriptions and instalment schedules automatically.
resubmission_requestedA reinstate request was made via the mandate reinstate endpointShow the mandate as pending
reinstatedThe mandate has become active again after being cancelled or expiredShow the mandate as active
replacedThe mandate was cancelled and replaced by a new one. The event includes links.new_mandateUpdate the stored mandate ID to the value in links.new_mandate
transferredThe mandate moved to a new bank accountFetch the new bank account from links.new_customer_bank_account and update the bank details you show your user
blockedThe mandate matched fraud information or a block list and cannot be unblockedShow the mandate as blocked

mandate-state-diagram

See the full list of mandate webhooks in the API reference.

Payment webhooks#

EventDescriptionSuggested handling
createdThe payment has been createdSave the payment ID against the record
submittedThe payment has been submitted to the banksShow the payment as pending
confirmedThe payment has been collected and is held by GoCardless before payoutMark the record as paid
paid_outThe payment has been sent to the creditor's bank accountMark the record as paid out and link it to its payout, using links.payout
failedThe payment could not be collected. If will_attempt_retry is true, Success+ will retry automaticallyIf will_attempt_retry is true, show "Retry scheduled". Otherwise, show the reason, mark the record as unpaid, and let your user retry the payment
cancelledThe payment was cancelledShow the payment as cancelled, and mark the record as unpaid
resubmission_requestedSuccess+ has scheduled an automatic retry (see failed)Show "Retry scheduled"
charged_backThe customer reclaimed the payment via the Direct Debit GuaranteeMark the record as unpaid, and show the chargeback to your user
chargeback_cancelledThe customer's bank cancelled the chargeback requestMark the record as paid again

payment-state-diagram

For what to show your user in each situation, see when payments go wrong. See the full list of payment webhooks in the API reference.

Refund webhooks#

EventDescriptionSuggested handling
createdA refund has been created (details.cause: payment_refunded, details.origin: api)Show the refund as pending next to the payment
failedThe refund did not reach the customer; funds are returned to your user (details.cause: refund_failed)Show the refund as failed, so your user can contact the payer
paidThe refund has been paid to the customer (details.cause: refund_paid)Show the refund as paid

See the full list of refund webhooks in the API reference.

Disconnections#

Your user can revoke your access, or their GoCardless account can be closed. When this happens, we send you a final webhook, so you can clean up any data on your side. Your access token stops working, so you can't make any more API calls for that user. If you think the disconnection was a mistake, contact the user directly.

This webhook looks like other webhooks, wrapped in the events key, but there's no associated event, so the event id is null. You can identify it by its resource_type of organisations and action of disconnected.

For example, a handler for this webhook:

POST https://example.com/webhooks HTTP/1.1
Content-Type: application/json
{
  "events": [
    {
      "id": null,
      "created_at": "2018-08-03T12:00:00.000Z",
      "action": "disconnected",
      "resource_type": "organisations",
      "links": {
        "organisation": "OR123"
      },
      "details": {
        "cause": "app_disconnected",
        "origin": "gocardless",
        "description": "Your app has been has disconnected from this organisation"
      },
      "metadata": {}
    }
  ]
}
<?php
function process_organisation_event($event)
{
    switch ($event->action) {
        case "disconnected":
            print("Organisation " . $event->links->organisation . " has been disconnected!\n");
            MarkOrganisationAsDisconnected($event->links->organisation);
            break;
        default:
            print("Don't know how to process an organisation " . $event->action . " event\n");
            break;
    }
}

$webhook_endpoint_secret = getenv("GOCARDLESS_WEBHOOK_ENDPOINT_SECRET");
$request_body = file_get_contents('php://input');

$headers = getallheaders();
$signature_header = $headers["Webhook-Signature"];

try {
    $events = \GoCardlessPro\Webhook::parse(
        $request_body,
        $signature_header,
        $webhook_endpoint_secret
    );

    foreach ($events as $event) {
        print("Processing event " . $event->id . "\n");

        switch ($event->resource_type) {
            case "organisations":
                process_organisation_event($event);
                break;
            default:
                print("Don't know how to process an event with resource_type " . $event->resource_type . "\n");
                break;
        }
    }

    header("HTTP/1.1 200 OK");
} catch (\GoCardlessPro\Core\Exception\InvalidSignatureException $e) {
    header("HTTP/1.1 498 Invalid Token");
}
# Here, we're using a Django view, but essentially the same code will work in
# other Python web frameworks (e.g. Flask) with minimal changes
import os
import logging

from django.views.generic import View
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.http import HttpResponse

from gocardless_pro import webhooks, Client
from gocardless_pro.errors import InvalidSignatureError

from myinvoicingapp.models import GoCardlessEvent

# Handle the incoming Webhook and perform an action with the Webhook data.
class Webhook(View):
    logger = logging.getLogger(**name**)

    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super(Webhook, self).dispatch(*args, **kwargs)

    def post(self, request, *args, **kwargs):
        try:
            for event in self._get_events(request):
                self.process(event)
            return HttpResponse(204)
        except InvalidSignatureError:
            return HttpResponse(498)

    def process(self, event):
        self.logger.info("Processing event {}\n".format(event.id))

        if event.resource_type == 'organisations':
            return self.process_organisation_event(event)
        # ... Handle other resource types
        else:
            self.logger.info("Don't know how to process an event with \
                resource_type {}\n".format(event.resource_type))

    def process_organisation_event(self, event):
        if event.action == 'disconnected':
            self.logger.info(
                "Organisation {} has been disconnected\n".
                format(event.links.organisation)
            )
        else:
            self.logger.info("Don't know how to process an event with \
                resource_type {}\n".format(event.resource_type))

    def _get_events(self, request):
        secret = os.environ['GC_WEBHOOK_SECRET']
        signature = request.META["HTTP_WEBHOOK_SIGNATURE"]
        body = request.body.strip()
        return webhooks.parse(body, secret, signature)
require 'gocardless_pro'

class OrganisationEventProcessor
  def self.process(event, response)
    case event.action
    when 'disconnected'
      response.stream.write("Organisation #{event.links.organisation} has been disconnected\n")

      DisconnectOrganisation.enqueue(event['links']['organisation'])
    else
      response.stream.write(
        "Don't know how to process a organisation #{event.action} event\n"
      )
    end
  end
end

def create
  # We recommend storing your webhook endpoint secret in an environment variable
  # for security
  webhook_endpoint_secret = ENV['GOCARDLESS_WEBHOOK_ENDPOINT_SECRET']

  # In a Rack app (e.g. Sinatra), access the POST body with
  # `request.body.tap(&:rewind).read`
  request_body = request.raw_post

  # In a Rack app (e.g. Sinatra), the header is available as
  # `request.env['HTTP_WEBHOOK_SIGNATURE']`
  signature_header = request.headers['Webhook-Signature']

  begin
    events = GoCardlessPro::Webhook.parse(
      request_body: request_body,
      signature_header: signature_header,
      webhook_endpoint_secret: webhook_endpoint_secret
    )

    events.each do |event|
      # We're using Rails's streaming functionality here to write directly to the
      # response rather than using views, as one would usually.
      response.stream.write("Processing event #{event.id}\n")

      case event.resource_type
      when 'organisations'
        OrganisationEventProcessor.process(event, response)
      else
        response.stream.write(
          "Don't know how to process an event with resource_type #{event.resource_type}\n"
        )
      end
    end

    response.stream.close
    render status: 200
  rescue GoCardlessPro::Webhook::InvalidSignatureError
    render status: 498
  end
end
package hello;

// Use the POM file at
// https://raw.githubusercontent.com/gocardless/gocardless-pro-java-maven-example/master/pom.xml

import com.gocardless.errors.InvalidSignatureException;
import com.gocardless.resources.Event;
import com.gocardless.Webhook;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;

@RestController
public class HelloController {
    private String processDisconnections(Event event) {
        switch (event.getAction()) {
            case "disconnected":
                // here, you probably want to mark the organisation as disconnected
                // and stop sending any requests on their behalf
                return "Organisation " + event.getLinks().getOrganisation() +
                    " has been cancelled.\n";
            default:
                return "Do not know how to process an event with action " +
                    event.getAction() + ".\n";
        }
    }

    private String processEvent(Event event) {
        switch (event.getResourceType()) {
            case ORGANISATIONS:
                return processDisconnections(event);
            default:
                return "Don't know how to process an event of resource_type " +
                    event.getResourceType().toString() + ".\n";
        }
    }

    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signatureHeader,
            @RequestBody String requestBody) {
        String webhookEndpointSecret = System.getenv("GOCARDLESS_WEBHOOK_ENDPOINT_SECRET");

        try {
            List<Event> events = Webhook.parse(requestBody, signatureHeader, webhookEndpointSecret);

            String responseBody = "";

            for (Event event : events) {
                responseBody += processEvent(event);
            }

            return new ResponseEntity<String>(responseBody, HttpStatus.OK);
        } catch (InvalidSignatureException e) {
            return new ResponseEntity<String>("Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}
Tip
Retry failed webhooks from the Partner Dashboard

If a webhook fails to deliver, you can view its status and retry it directly from the Partner Dashboard. Navigate to a merchant, payments tab, click on the payment, and you'll find the webhook status and retry option there.

What's next?#