Once you’ve set up mandates, you’ll need to stay up to date as they change. For example, a customer can cancel their Direct Debit at any time by contacting their bank, and you’ll want to reflect this in your product.

To help you handle this, GoCardless will send you webhooks whenever something happens in one of your users’ GoCardless accounts. Adding support for webhooks allows you to receive real-time notifications, so you can take automated actions in response, for example:

  • When a customer cancels their mandate with the bank, suspend their club membership
  • When a payment fails due to lack of funds, mark their invoice as unpaid
  • When a customer’s subscription generates a new payment, log it in their “past payments” list
Webhooks are not suitable for all applications. For alternative strategies, see below.

Let’s look at how to build a simple webhook handler. This important foundational work will help us later to take action when things happen to our mandates and payments.

Setting your webhook URL

To start experimenting with webhooks, your application will need to be accessible from the internet so GoCardless can reach it with HTTP requests. If you’re working locally, the easiest way to do this is with ngrok.

Download it and follow the instructions on the page to install it (you’ll need to unzip the archive, and then make the ngrok binary available in your PATH).

Now, just run ngrok http -bind-tls=true <port>, where <port> is the port where your web server is running (for example ngrok http http -bind-tls=true 8000 if you’re running on port 8000). You’ll see the externally-accessible URL that forwards to your localhost - copy it to your clipboard.

Making your local environment accessible with ngrok

In the sandbox, you can use an HTTP URL for your webhooks, but to go live, the endpoint must be HTTPS secured.

To start receiving webhooks, you’ll need to add this as your app’s webhook URL in the Dashboard here.

Simply enter the HTTPS URL from earlier and add on the path where your webhook handler will be available, then click “Update App”. Next, copy the secret from the “Webhook Details” section which will appear for your app.

Setting a webhook URL

Building your first webhook handler

Let’s get started with building a framework for handling webhooks - it’ll handle the webhook event GoCardless sends you when a customer cancels their mandate.

Webhooks are HTTP POST requests made to the URL you provided, with a JSON body, looking something like this:

POST https://example.com/webhooks HTTP/1.1
User-Agent: gocardless-webhook-service/1.1
Content-Type: application/json
Webhook-Signature: 78e3507f61f141046969c73653402cb50b714f04322da04d766ee0f6d2afe65f

{
  "events": [
    {
      "id": "EV123",
      "created_at": "2014-08-04T12:00:00.000Z",
      "action": "cancelled",
      "resource_type": "mandates",
      "links": {
        "mandate": "MD123",
        "organisation": "OR123"
      },
      "details": {
        "origin": "bank",
        "cause": "bank_account_disabled",
        "description": "Your customer closed their bank account.",
        "scheme": "bacs",
        "reason_code": "ADDACS-B"
      }
    }
  ]
}

The body contains one or more events, specifying the type of the resource affected (e.g. “mandates” or “payments”), its ID, the ID of the organisation the resource belongs to (which you stored when obtaining their access token) and the action that occurred (e.g. “cancelled” or “expired”).

The first step to take when you receive a webhook is to check its signature - this makes sure that is genuinely from GoCardless and hasn’t been forged. A signature is provided in the Webhook-Signature header of the request. We just need to compute the signature ourselves using the POSTed JSON and the webhook endpoint’s secret (which we copied earlier), and compare it to the one in the header.

If they match, the webhook is genuine, because only you and GoCardless know the secret. It’s important that you keep the secret safe and private.

We can verify the signature like this:

<?php
// We recommend storing your webhook endpoint secret in an environment variable
// for security, but you could include it as a string directly in your code
$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);

    // Process the events...

    header("HTTP/1.1 204 No Content");
} 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

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
from gocardless_pro.errors import InvalidSignatureError

# Handle the incoming Webhook and check its signature.
class Webhook(View):
    @method_decorator(csrf_exempt)
    def dispatch(self, *args, **kwargs):
        return super(Webhook, self).dispatch(*args, **kwargs)

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

    def post(self, request, *args, **kwargs):
        try:
            for event in self.get_events(request):
                # Do something with the events ...
                pass

            return HttpResponse(200)
        except InvalidSignatureError:
            return HttpResponse(498)
# Here, we're using a Rails controller, but essentially the same code will work in other
# Ruby web frameworks (e.g. Sinatra) with minimal changes

class WebhooksController < ApplicationController
  include ActionController::Live

  protect_from_forgery except: :create

  def create
    # We recommend storing your webhook endpoint secret in an environment variable
    # for security, but you could include it as a string directly in your code
    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)

      # Process the events...

      render status: 204, nothing: true
    rescue GoCardlessPro::Webhook::InvalidSignatureError
      render status: 498, nothing: true
    end
  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 {
    @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)

            for (Event event : events) {
                // Process the event
            }

            return new ResponseEntity<String>("OK", HttpStatus.OK);
        } catch(InvalidSignatureException e) {
            return new ResponseEntity<String>("Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}
[HttpPost]
public ActionResult HandleWebhook()
{
    var requestBody = Request.InputStream;
    requestBody.Seek(0, System.IO.SeekOrigin.Begin);
    var requestJson = new StreamReader(requestBody).ReadToEnd();

    // We recommend storing your webhook endpoint secret in an environment variable
    // for security, but you could include it as a string directly in your code
    var secret = ConfigurationManager.AppSettings["GoCardlessWebhookSecret"];
    var signaure = Request.Headers["Webhook-Signature"] ?? "";

    try
    {
        foreach (Event event in WebhookParser.Parse(requestJson, secret, signature))
        {
            // Process the event
        }

        return new HttpStatusCodeResult(HttpStatusCode.OK);
    }
    catch (InvalidSignatureException e)
    {
        return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
    }
}

Now, let’s add in some basic logic to process mandate cancellations (where resource_type is mandates and action is cancelled). Here, you’ll:

  • Calculate the correct signature using our secret and the request body, and compare that to the one in the headers
  • For each event in the body:
    • Look up the ID in your database to make sure you haven’t processed it before. You’ll need to keep a record to make sure you only process each event once.
    • Enqueue a job to asynchronously perform any actions you want to perform (e.g. emailing your user to let them know about the cancellation)
    • Record in the database that you’ve processed the event
  • Respond with a successful response (204 No Content). If you respond with an HTTP error code, or the request from GoCardless to your server times out, GoCardless will retry up to ten times at increasing intervals.
<?php
function process_mandate_event($event, $client)
{
    $mandate = $client->mandates()->get($event->links["mandate"]);

    switch ($event->action) {
    case "cancelled":
        print("Mandate " . $mandate->id . " has been cancelled!\n");

        // You should perform processing steps asynchronously to avoid timing out if
        // you receive many events at once. To do this, you could use a queueing
        // system like Beanstalkd
        //
        // http://george.webb.uno/posts/sending-php-email-with-mandrill-and-beanstalkd
        CancelServiceAndNotifyCustomer::performAsynchronously($mandate->id);
        GoCardlessEvent::create(['gocardless_id' => $event->id]);
        break;
    default:
        print("Don't know how to process a mandate " . $event->action . " event\n");
        break;
  }
}

function is_already_processed($event)
{
    return GoCardlessEvent::where(['gocardless_id' => $event->id])->count() > 0;
}

// We recommend storing your webhook endpoint secret in an environment variable
// for security
$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);

    // Each webhook may contain multiple events to handle, batched together
    foreach ($events as $event) {
        print("Processing event " . $event->id . "\n");

        $organisation_id = $event->links["organisation"];
        $access_token = User::where(['gocardless_organisation_id' => $organisation_id])
          ->firstOrFail()
          ->gocardless_access_token;

        $client = new \GoCardlessPro\Client([
            'access_token' => $access_token,
            'environment' => \GoCardlessPro\Environment::SANDBOX
        ]);

        if (is_already_processed($event)) {
            continue;
        }

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

    header("HTTP/1.1 204 No Content");
} 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 json
import hmac
import hashlib
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 myinvoicingapp.models import GoCardlessEvent
from myinvoicingapp.models import User

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

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

    def is_valid_signature(self, request):
        secret = bytes(os.environ['GC_WEBHOOK_SECRET'], 'utf-8')
        computed_signature = hmac.new(
            secret, request.body, hashlib.sha256).hexdigest()
        provided_signature = request.META["HTTP_WEBHOOK_SIGNATURE"]
        return hmac.compare_digest(provided_signature, computed_signature)

    def post(self, request, *args, **kwargs):
        if self.is_valid_signature(request):
            payload = json.loads(request.body.decode('utf-8'))

            # Each webhook may contain multiple events to handle, batched together.
            for event in payload['events']:
                self.process(event)
            return HttpResponse(204)
        else:
            return HttpResponse(498)

    def is_already_processed(event):
        return GoCardlessEvent.objects.filter(gocardless_id=event['id']).exists()

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

        if is_already_processed(event):
            return

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

    def process_mandate_event(self, event):
        organisation_id = event['links']['organisation']

        access_token = User.objects.get(gocardless_organisation_id=organisation_id).gocardless_access_token

        client = gocardless_pro.Client(
            access_token=access_token,
            environment='sandbox'
        )

        mandate = client.mandates.get(event['id'])

        if event['action'] == 'cancelled':
            # You should perform processing steps asynchronously to avoid timing out
            # if you receive many events at once. To do this, you could use a
            # queueing system like RQ (https://github.com/ui/django-rq)
            logger.info("Mandate {} has been cancelled\n".format(mandate.id))
        # ... Handle other mandate actions
        else:
            logger.info("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))

        GoCardlessEvent.objects.create(gocardless_id=mandate.id)

        return response
require 'gocardless_pro'

class MandateEventProcessor
  def self.process_mandate_event(event)
    return if already_processed?(event)

    organisation_id = event.links.organisation

    access_token = User.find_by(gocardless_organisation_id: organisation_id).access_token
    gocardless = GoCardlessPro::Client.new(access_token: access_token,
                                           environment: :sandbox)
    mandate = gocardless.mandates.get(event.links.mandate)

    case event.action
    when 'cancelled'
      Rails.logger.info("Mandate #{mandate.id} has been cancelled\n")

      # You should perform processing steps asynchronously to avoid timing out
      # if you receive many events at once. To do this, you could use a
      # queueing system like Resque (https://github.com/resque/resque)
      CancelServiceAndNotifyCustomer.enqueue(event.links.mandate)

      GoCardlessEvent.create!(gocardless_id: event.id)
    else
      Rails.logger.info("Don't know how to process a mandate #{event.action} event\n")
    end

    def self.already_processed?(event)
      GoCardlessEvent.where(gocardless_id: event.id).any?
    end
  end
end

def create
  # We recommend storing your webhook endpoint secret in an environment variable
  # for security, but you could include it as a string directly in your code
  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|
      case event.resource_type
      when 'mandates'
        MandateEventProcessor.process(event)
      else
        Rails.logger.info("Don't know how to process an event with resource_type " \
                          "#{event.resource_type}\n")
      end
    end

    render status: 204, nothing: true
  rescue GoCardlessPro::Webhook::InvalidSignatureError
    render status: 498, nothing: true
  end
end
package com.myInvoicingApp;

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

import com.gocardless.resources.Event;
import com.myInvoicingApp.GoCardlessEventLog;
import com.myInvoicingApp.MandateWebhookHandler;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

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

            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);
        }
    }

    private String processEvent(Event event) {
        if GoCardlessEventLog.alreadyProcessed(event.getId()) {
          return "Event already processed";
        }
        switch (event.getResourceType()) {
            case MANDATES:
                return processMandateEvent(event);
            default:
                return "Don't know how to process an event of resource_type " +
                    event.getResourceType().toString() + ".\n";
        }
    }

    private String processMandateEvent(Event event) {
    /*
      You should keep some kind of record of what events have been processed
      to avoid double-processing, checking if the event already exists
      before processing it.

      You should perform processing steps asynchronously to avoid timing out
      if you receive many events at once. To do this, you could use a
      queueing system like @Async.

      https://spring.io/guides/gs/async-method/

      Once you've performed the actions you want to perform for an event, you
      should make a record to avoid accidentally processing the same one twice
    */
        switch (event.getAction()) {
            case "cancelled":
                MandateWebhookHandler.handleCancelledEvent(event);
                GoCardlessEventLog.save(event.getId());
                return "Mandate " + event.getLinks().getMandate() +
                    " has been cancelled.\n";
            default:
                return "Do not know how to process an event with action " +
                    event.getAction() + ".\n";
        }
    }
}
[HttpPost]
public ActionResult HandleWebhook()
{
    variable requestBody = Request.InputStream;
    requestBody.Seek(0, System.IO.SeekOrigin.Begin);
    var requestJson = new StreamReader(requestBody).ReadToEnd();

    // We recommend storing your webhook endpoint secret in an environment variable
    // for security, but you could include it as a string directly in your code
    var secret = ConfigurationManager.AppSettings["GoCardlessWebhookSecret"];

    var hmac256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    byte[] messageHash = hmac256.ComputeHash(Encoding.UTF8.GetBytes(requestJson));
    var result = BitConverter.ToString(messageHash).Replace("-", "").ToLower();

    // If the signature doesn't match what was expected, reject the request
    // Otherwise, handle the events in the webhook then respond with 200 OK
    if ((Request.Headers["Webhook-Signature"] ?? "") != result)
      return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

    // Here we deserialize the webhook using the same objects the library uses to
    // represent API responses for Events, but you could parse and handle the JSON
    // body however you prefer.
    var serializerSettings = new GoCardless.Internals.JsonSerializerSettings();
    GoCardless.Services.EventListResponse events =
        JsonConvert.DeserializeObject<GoCardless.Services.EventListResponse>(message, serializerSettings);

    // Webhooks can contain many events. In a real implementation, you should handle
    // the processing of each event asychronously to avoid timing out here.
    // You could check whether you've processed the event before (by recording the event
    // ID when you process it) and retrieve the associated resource to ensure that you have
    // the most up-to-date information about it.
    foreach (GoCardless.Resources.Event eventResource in events.Events)
    {
        // To keep this example simple, we're only handling Mandate events
        if (eventResource.ResourceType == GoCardless.Resources.EventResourceType.Mandates)
        {
            switch (eventResource.Action)
            {
                case "created":
                    Console.WriteLine($"Mandate {eventResource.Links.Mandate} has been created, yay!");
                    break;
                case "cancelled":
                    Console.WriteLine($"Oh no, mandate {eventResource.Links.Mandate} was cancelled!");
                    break;
                default:
                    Console.WriteLine($"{eventResource.Links.Mandate} has been {eventResource.Action}");
                    break;
            }
        }
        else
        {
            Console.WriteLine($"No method to handle {eventResource.ResourceType} events yet");
        }
    }
    return new HttpStatusCodeResult(HttpStatusCode.OK);
}

Testing your webhook handler

In the Dashboard, the “Send test web hook” functionality makes it easy to start experimenting with webhooks.

Select your app, set the “Resource type” to mandates, the “action” to cancelled, the “cause” and “event details” to whatever you want and enter an organisation ID. Then, click “Send test webhook”.

Sending a test GoCardless webhook

GoCardless will make a request to your endpoint, and full details of the request and response will appear in the list - click on it. If everything’s working, you’ll see a response code of 204 No Content.

We’ve now built the basic structure of a webhook handler. In the next section, we’ll look at how to effectively handle the most important parts of the lifecycle of a mandate.

Keeping up to date when you cannot use webhooks

While the webhook system is the best way to keep your integration in sync with the transactions taking place on the GoCardless platform, it is not ideal for all use cases.

Webhooks require - by definition - some part of your integration to be accessible on the public internet. This is a very good fit, for example, for web-based software-as-a-service applications. However, rich-client native apps will not usually be able to use webhooks. For on-premise software deployed on a private network, it is necessary to deploy a publicly-available proxy, which may not be practical.

In these situations, you can use the events API to poll for changes on a periodic basis. You can use GoCardless’s cursor-based pagination system to ensure that you only get new events, as long as you store the ID of the last event you have seen, by using the after query parameter. (Alternatively, if you wish to filter by a particular time, you can use the created_at[gt] parameter instead.)

// Use the after parameter to exclude events before the last one your system has
// successfully polled
$client->events()->all([params => [ "after" => "EV123" ]]);

// Use the created_at[gt] parameter to filter by time
$client->events()->all([
  params => [ "created_at[gt]" => "2018-10-01T10:00:00.000Z" ]
]);

# Use the after parameter to exclude events before the last one your system has
# successfully polled
client.events.all(params={ 'after': 'EV123' })

# Use the created_at[gt] parameter to filter by time
client.events.all(params={ "created_at[gt]": "2018-10-01T10:00:00.000Z" })
# Use the after parameter to exclude events before the last one your system has
# successfully polled
@client.events.all(params: { after: "EV123" })

# Use the created_at[gt] parameter to filter by time
@client.events.all(params: { "created_at[gt]": "2018-10-01T10:00:00.000Z" })
// Use the after parameter to exclude events before the last one your system has
// successfully polled
client.events().all().withAfter("EV123").execute();

// Use the created_at[gt] parameter to filter by time
client.events().all().withCreatedAtGt("2018-10-01T10:00:00.000Z").execute();
// Use the after parameter to exclude events before the last one your system has
// successfully polled
var eventRequest = new GoCardless.Services.EventListRequest()
{
    After = "EV123"
};

var eventListResponse = gocardless.Events.All(eventRequest);

// Use the created_at[gt] parameter to filter by time
var eventRequest = new GoCardless.Services.EventListRequest()
{
    CreatedAt = new GoCardless.Services.EventListRequest.CreatedAtParam()
    {
        GreaterThan = "2018-10-01T10:00:00.000Z"
    }
};

var eventListResponse = gocardless.Events.All(eventRequest);
GET https://api.gocardless.com/events?after=EV123 HTTP/1.1

HTTP/1.1 200 (OK)
{
  "meta": {
    "cursors": {
      "after": null,
      "before": null
    },
    "limit": 50
  },
  "events":[
    {
      "id": "EV456",
      "created_at": "2014-04-08T17:01:06.000Z",
      "resource_type": "mandates",
      "action": "cancelled",
      "details": {
        "origin": "bank",
        "cause": "bank_account_disabled",
        "description": "Customer's bank account closed",
        "scheme": "bacs",
        "reason_code": "ADDACS-B"
      },
      "metadata": {},
      "links": {
        "mandate": "MD123"
      }
    },
    {
      "id": "EV789",
      "created_at": "2014-04-08T17:01:06.000Z",
      "resource_type": "payments",
      "action": "cancelled",
      "details": {
        "origin": "bank",
        "cause": "mandate_cancelled",
        "description": "The mandate for this payment was cancelled at a bank branch.",
        "scheme": "bacs",
        "reason_code": "ADDACS-B"
      },
      "metadata": {},
      "links": {
        "payment": "PM123",
        "parent_event": "EV123"
      }
    }
  ]
}

Please note that some features - for example, handling customer notifications - require you to use the webhook system.

It’s also worth noting that the majority of events are created when we receive (8-11am BST) and send (3-6pm BST) reports to the bank. Therefore we usually suggest querying the endpoint around midday, then in the evening and any other periods you wish.