GoCardlessDeveloper Docs
Create a sandbox account

Stay up to date with webhooks#

View as Markdown

A webhook is a 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 things happen in your account, so you can take automated actions in response.

  • When a payment fails due to lack of funds, retry it automatically.
  • When a customer cancels their mandate with the bank, suspend their account.
  • When a subscription generates a new payment, record it against their account.

Create a webhook endpoint#

To start receiving webhooks, add a webhook endpoint from your Sandbox Dashboard.

Creating a webhook endpoint in the GoCardless dashboard

Enter the HTTPS URL where your webhook handler will be available, give your endpoint a name, and click "Create webhook endpoint". Then click on your new endpoint in the list and copy the secret to your clipboard.

Build a webhook handler#

Webhooks are HTTP POST requests made to the URL you provided, with a JSON body. Here's an example:

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"
      }
    }
  ]
}

Secure your webhooks#

The first step when you receive a webhook is to check its signature — this ensures it's genuinely from GoCardless and hasn't been forged. A signature is provided in the Webhook-Signature header. Compute the signature yourself using the POST body and your webhook endpoint secret, then compare it to the header value.

If they match, the webhook is genuine. Keep the secret safe and change it periodically using the Dashboard.

<?php
// 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 = array_change_key_case(getallheaders(), CASE_LOWER);
$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.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
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);
      }
  }

}
const process = require("process");
const webhooks = require("gocardless-nodejs/webhooks");

const webhookEndpointSecret = process.env.WEBHOOK_ENDPOINT_SECRET;

// Handle the incoming Webhook and check its signature.
const parseEvents = (
eventsRequestBody,
signatureHeader, // From webhook header
) => {
try {
return webhooks.parse(
eventsRequestBody,
webhookEndpointSecret,
signatureHeader,
);
} catch (error) {
if (error instanceof webhooks.InvalidSignatureError) {
console.log("invalid signature, look out!");
}
}
};
[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
  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);
  }

}
package main

import (
"http"

  gocardless "github.com/gocardless/gocardless-pro-go/v6"

)

func main() {
http.HandleFunc("/webhookEndpoint", func(w http.ResponseWriter, r *http.Request) {
wh, err := NewWebhookHandler("secret", EventHandlerFunc(func(event gocardless.Event) error {
parseEvent(event)
}))
wh.ServeHTTP(w, r)
})
}

func parseEvents(event gocardless.Event) {
// work through list of events
}

Processing events#

A webhook can contain multiple events, each with a resource_type (e.g. "payments" or "mandates"), an action (e.g. "cancelled"), and details (specifying why the event happened).

You can see a full list of possible combinations in the event types reference.

Here's an example handler for when a mandate is cancelled:

<?php
function process_mandate_event($event)
{
  switch ($event->action) {
      case "cancelled":
          print("Mandate " . $event->links->mandate . " has been cancelled!\n");

          // You should keep some kind of record of what events have been processed
          // to avoid double-processing
          //
          // $event = Event::where("gocardless_id", $event->id)->first();

          // 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
          CancelServiceAndNotifyCustomer::performAsynchronously($event->links->mandate);
          break;
      default:
          print("Don't know how to process a mandate " . $event->action . " event\n");
          break;
  }

}

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

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

      switch ($event->resource_type) {
          case "mandates":
              process_mandate_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 is_already_processed(event):
      return GoCardlessEvent.objects.filter(gocardless_id=event.id).exists()

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

      if self.is_already_processed(event):
          return

      if event.resource_type == 'mandates':
          return self.process_mandate_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_mandate_event(self, event):
      access_token = os.environ('GOCARDLESS_ACCESS_TOKEN')

      client = 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)
          self.logger.info("Mandate {} has been cancelled\n".format(mandate.id))
      # ... Handle other mandate actions
      else:
          self.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)

  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 MandateEventProcessor
def self.process(event, response)
case event.action
when 'cancelled'
response.stream.write("Mandate #{event.links.mandate} has been cancelled\n")

    # You should keep some kind of record of what events have been processed
    # to avoid double-processing, checking if the event already exists before
    # event = Event.find_by(gocardless_id: event['id'])

    # 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)
    #
    # 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
    # CancelServiceAndNotifyCustomer.enqueue(event['links']['mandate'])
  else
    response.stream.write("Don't know how to process a mandate #{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 'mandates'
      MandateEventProcessor.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 processMandate(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":
              return "Mandate " + event.getLinks().getMandate() +
                  " 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 MANDATES:
              return processMandate(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);
      }
  }

}
const process = require("process");
const webhooks = require("gocardless-nodejs/webhooks");
const express = require("express");
const bodyParser = require("body-parser");

const app = express();
// parse incoming body of Content-Type of application/json to return it as a plain string which needs to be passed into the webhooks.parse method
app.use(bodyParser.text({ type: "application/json" }));

const webhookEndpointSecret = process.env.WEBHOOK_ENDPOINT_SECRET;

const processMandate = (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

# Bull https://github.com/OptimalBits/bull

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.action) {
case "cancelled":
return `Mandate ${event.links.mandate} has been cancelled.\n`;
default:
return `Do not know how to process an event with action ${event.action}`;
}
};

const processEvent = (event) => {
switch (event.resource_type) {
case "mandates":
return processMandate(event);
default:
return `Do not know how to process an event with resource_type ${event.resource_type}`;
}
};

app.post("/", async (req, res) => {
const eventsRequestBody = req.body;
const signatureHeader = req.headers["Webhook-Signature"];

// Handle the incoming Webhook and check its signature.
const parseEvents = (
eventsRequestBody,
signatureHeader, // From webhook header
) => {
try {
return webhooks.parse(
eventsRequestBody,
webhookEndpointSecret,
signatureHeader,
);
} catch (error) {
if (error instanceof webhooks.InvalidSignatureError) {
console.log("invalid signature, look out!");
}
}
};

try {
const events = parseEvents(eventsRequestBody, signatureHeader);
let responseBody = "";
for (const event of events) {
responseBody += processEvent(event);
}

  res.send(responseBody);

} catch (error) {
res.status(400).send({ error: error.message });
}
});

app.listen(process.env.PORT, () => {
console.log(`server started on port: ${process.env.PORT}`);
});
[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
  var secret = ConfigurationManager.AppSettings["GoCardlessWebhookSecret"];
  var signature = Request.Headers["Webhook-Signature"];

  try
  {
      // 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 event in WebhookParser.Parse(requestJson, secret, signature))
      {
          // To keep this example simple, we're only handling Mandate events
          if (event.ResourceType == GoCardless.Resources.EventResourceType.Mandates)
          {
              switch (event.Action)
              {
                  case "created":
                      Console.WriteLine($"Mandate {event.Links.Mandate} has been created, yay!");
                      break;
                  case "cancelled":
                      Console.WriteLine($"Oh no, mandate {event.Links.Mandate} was cancelled!");
                      break;
                  default:
                      Console.WriteLine($"{event.Links.Mandate} has been {event.Action}");
                      break;
              }
          }
          else
          {
              Console.WriteLine($"No method to handle {event.ResourceType} events yet");
          }
      }
      return new HttpStatusCodeResult(HttpStatusCode.OK);

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

}
package main

import (
"fmt"
"http"

  gocardless "github.com/gocardless/gocardless-pro-go/v6"

)

func main() {
processMandateEvent := func(event gocardless.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 dispatch
  	   each event to a worker pool of goroutines, or push it onto a
  	   queueing system such as Amazon SQS or RabbitMQ.

  	   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.Action {
  	case "cancelled":
  		fmt.Println("Mandate " + event.Links.Mandate +
  			" has been cancelled.\n")
  	default:
  		fmt.Println("Do not know how to process an event with action " +
  			event.Action + ".\n")
  	}
  }

  parseEvent := func(event gocardless.Event) {
  	switch event.getResourceType() {
  	case "mandates":
  		processMandateEvent(event)
  	default:
  		fmt.Println("Don't know how to process an event of resource_type " +
  			event.ResourceType + ".\n")
  	}
  }

  http.HandleFunc("/webhookEndpoint", func(w http.ResponseWriter, r *http.Request) {
  	wh, err := gocardless.NewWebhookHandler("secret", gocardless.EventHandlerFunc(func(event gocardless.Event) error {
  		parseEvent(event)
  	}))
  	wh.ServeHTTP(w, r)
  })

}

Send a test webhook and click on it in the list. You'll see a response like this:

Processing event EVTEST8XP9DCPK
Mandate index_ID_123 has been cancelled!

We'll send webhooks exactly like this when real events happen in your account. You can add support for as many resource_types and actions as you like, using all of the event data we provide.

Test your webhook handler#

Advanced: Using TLS client authentication

Using TLS enables us to encrypt data in transit. Additional security can be achieved by using client authentication alongside server authentication.

Client authentication adds an extra check in the TLS handshake to validate that the client is authorised to access the endpoint — confirming that it is GoCardless making the request, not a malicious entity.

To authorise GoCardless to send webhooks, provide a valid certificate and private key issued by your certificate authority. In practice, this can be a self-signed certificate. Useful instructions for generating certificates can be found in the CoreOS documentation, using Cloudflare's cfssl toolkit.

Enter the certificate and key into the webhook endpoint form in the developers section of the GoCardless dashboard. All subsequent requests will be client-authenticated.

Webhook client certificate configuration

Next steps#