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, for example:

  • 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 customer’s subscription generates a new payment, record that payment against their account

Making localhost accessible to the internet with ngrok

To start experimenting with webhooks, your code 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 <port>, where <port> is the port where your web server is running (for example ngrok http 8000 if you’re running on port 8000).

You’ll see two links that forward to localhost:8000. Select the URL that starts with https, and 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.

Adding a webhook URL in the GoCardless Dashboard

To start receiving webhooks, you’ll need to add a webhook endpoint from your Dashboard here.

Simply enter the HTTPS URL from earlier and add on the path where your webhook handler will be available, give your endpoint a name, and then click “Create webhook endpoint”. Next, click on your new endpoint in the list and copy the secret to your clipboard.

Creating a GoCardless webhook endpoint

Building a webhook handler

Let’s get started by building our first webhook handler. Webhooks are HTTP POST requests made to the URL you provided, with a JSON body.

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 change it periodically using the Dashboard.

We can verify the signature like this - make the following code available at the URL you set earlier:

<?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
$token = getenv("GC_WEBHOOK_SECRET");

$raw_payload = file_get_contents('php://input');

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

$calculated_signature = hash_hmac("sha256", $raw_payload, $token);

if ($provided_signature == $calculated_signature) {
    // Process the events...

    header("HTTP/1.1 200 OK");
} else {
    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

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

# 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 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"]
        # In flask, access the webhook signature header with
        # request.headers.get('Webhook-Signature')
        return hmac.compare_digest(provided_signature, computed_signature)

    def post(self, request, *args, **kwargs):
        if self.is_valid_signature(request):
            return HttpResponse(200)
        else:
            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

require 'openssl'

class WebhooksController < ApplicationController
  protect_from_forgery except: :create
  include ActionController::Live

  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
    secret = ENV['GC_WEBHOOK_SECRET']

    # In a Rack app (e.g. Sinatra), access the POST body with
    # `request.body.tap(&:rewind).read`
    computed_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'),
                                                 secret,
                                                 request.raw_post)

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

    if Rack::Utils.secure_compare(provided_signature, computed_signature)
      render status: :ok
    else
      render status: 498
    end
  end
end
package myintegration;

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

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;
import org.apache.commons.codec.digest.HmacUtils;

@RestController
public class WebhookHandler {

    private boolean isValidSignature(String signature, String payload) {
        String secret = System.getenv("GC_WEBHOOK_SECRET");
        String computedSignature = HmacUtils.hmacSha256Hex(secret, payload).toString();
        return computedSignature.equals(signature);
    }

    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signature,
            @RequestBody String payload) {
        if (isValidSignature(signature, payload)) {
            return new ResponseEntity<String>("OK", HttpStatus.OK);
        } else {
            return new ResponseEntity<String>(
                "Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}

Testing your webhook handler

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

Select the webhook endpoint you created earlier, set the Resource type of mandates and the action to cancelled. You can set the cause and event details however you like. Then, click “Send test web hook”.

Sending a test GoCardless webhook

Now, refresh the page. Usually, the webhook will appear right away, but sometimes you might have to refresh a few times. We’ll make a request to your endpoint, and your webhook will appear in the list - click on it. If everything’s working, you’ll see a response code of 200 OK.

Processing events

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 cancelled action for a mandate) and details (specifying why the event happened).

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

As an example, we’ll write a 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
        //
        // http://george.webb.uno/posts/sending-php-email-with-mandrill-and-beanstalkd
        //
        // 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::performAsynchronously($event["links"]["mandate"]);
        break;
      default:
        print("Don't know how to process a mandate " . $event["action"] . " event\n");
        break;
  }
}

$token = getenv("GC_WEBHOOK_SECRET");

$raw_payload = file_get_contents('php://input');

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

$calculated_signature = hash_hmac("sha256", $raw_payload, $token);

if ($provided_signature == $calculated_signature) {
    $payload = json_decode($raw_payload, true);

    // Each webhook may contain multiple events to handle, batched together
    foreach ($payload["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");
} else {
    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

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

# Handle the incoming Webhook and perform an action with the  Webhook data.
class Webhook(View):
    @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):
            response = HttpResponse()
            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, response)
            return response
        else:
            return HttpResponse(498)

    def process(self, event, response):
        response.write("Processing event {}\n".format(event['id']))
        if event['resource_type'] == 'mandates':
            return self.process_mandates(event, response)
        # ... Handle other resource types
        else:
            response.write("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))
            return response

    def process_mandates(self, event, response):
        if event['action'] == 'cancelled':
            response.write("Mandate {} has been \
                cancelled\n".format(event['links']['mandate']))
        # ... Handle other mandate actions
        else:
            response.write("Don't know how to process an event with \
                resource_type {}\n".format(event['resource_type']))
        return response
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
  secret = ENV['GC_WEBHOOK_SECRET']
  computed_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'),
                                               secret,
                                               request.raw_post)
  provided_signature = request.headers['Webhook-Signature']

  if Rack::Utils.secure_compare(provided_signature, computed_signature)
    params['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: :ok
  else
    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 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;
import org.apache.commons.codec.digest.HmacUtils;

import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
import com.google.gson.GsonBuilder;
import com.google.gson.Gson;
import com.gocardless.resources.Event;

@RestController
public class HelloController {

    private boolean isValidSignature(String signature, String payload) {
        String secret = System.getenv("GC_WEBHOOK_SECRET");
        String computedSignature = HmacUtils.hmacSha256Hex(secret, payload).toString();
        return computedSignature.equals(signature);
    }

    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 ecent 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";
        }
    }

    class WebhookPayload {
        private List<Event> events;

        public List<Event> getEvents() {
            return events;
        }
    }
    private List<Event> parseEvents(String jsonPayload){
        Gson gson = new GsonBuilder()
            .setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES).create();
        return gson.fromJson(jsonPayload, WebhookPayload.class).getEvents();
    }

    @PostMapping("/")
    public ResponseEntity<String> handlePost(
            @RequestHeader("Webhook-Signature") String signature,
            @RequestBody String payload) {
        if (isValidSignature(signature, payload)) {
            String responseBody = "";
            for (Event event : parseEvents(payload)) {
                responseBody += processEvent(event);
            }
            return new ResponseEntity<String>(responseBody, HttpStatus.OK);
        } else {
            return new ResponseEntity<String>(
                "Incorrect Signature", HttpStatus.BAD_REQUEST);
        }
    }
}

Send a test webhook again, just like the one you sent before, and then click on it in the list. You’ll see a response at the bottom 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.

In the sandbox, to make integrating as easy as possible, we provide special customer names that trigger particular scenarios almost instantly. For example, if you create a customer with the given name “Penniless”, their payments will fail immediately, triggering an event. You can read about all these special triggers here.

You can add support for as many differ resource_types and actions as you like, and make use of all of the other data we give you with events.