GoCardlessDeveloper Docs
Create a sandbox account

Mandate Events#

View as Markdown

Mandate events are webhook notifications that GoCardless sends whenever a mandate's status changes. This guide covers the key events you should handle and what to do when they occur.

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 a full list of mandate events see the API reference. For general guidance on receiving and processing webhooks see Webhooks overview.

Storing mandates

Keep a mandates table in your database linked to your internal customer records. Store the GoCardless mandate ID and status so you can track the full lifecycle and react to events.

Mandate lifecycle#

A mandate moves through the following statuses:

StatusMeaning
pending.submissionMandate created, not yet sent to the banks
submittedSent to the banks for processing
activeConfirmed by the banks, ready to collect against
failedCould not be set up (e.g. invalid bank details)
cancelledCancelled by the customer, merchant, or GoCardless
expiredExpired due to inactivity (scheme-dependent)
consumedUsed for a one-off payment and can no longer be reused

Events to handle#

Mandate cancelled#

Mandates can be cancelled by the customer via their bank, by you via the API, or by GoCardless. When this happens you'll receive:

{
  "events": {
    "id": "EV123",
    "resource_type": "mandates",
    "action": "cancelled",
    "links": {
      "mandate": "MD123"
    },
    "details": {
      "origin": "bank",
      "cause": "bank_account_closed",
      "description": "The bank account for this mandate has been closed."
    }
  }
}

The details.cause field tells you why the mandate was cancelled. Common values:

CauseMeaning
bank_account_closedCustomer's bank account has been closed
invalid_bank_detailsBank details were incorrect
direct_debit_not_enabledAccount doesn't support Direct Debit
customer_approval_deniedCustomer cancelled at their bank
mandate_cancelledCancelled via the GoCardless dashboard or API

When you receive this event you should:

  • Update the mandate status in your database
  • Prevent further payment attempts against this mandate
  • Notify the customer that their Direct Debit has been cancelled and prompt them to set up a new one if needed

Cancelling a mandate yourself:

If you need to cancel a mandate programmatically:

<?php
require 'vendor/autoload.php';

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

$mandate = $client->mandates()->cancel($mandate_id);
print("Status: " . $mandate->status);
import gocardless_pro

def cancel_mandate(current_user, mandate_id):
    client = gocardless_pro.Client(
        access_token=current_user.gocardless_access_token,
        environment='sandbox'
    )

    mandate = client.mandates.cancel(mandate_id)
    print("Status: {}".format(mandate.status))
require 'gocardless_pro'

def cancel_mandate(current_user, mandate_id)
  client = GoCardlessPro::Client.new(
    access_token: current_user.gocardless_access_token,
    environment: :sandbox
  )

  mandate = client.mandates.cancel(mandate_id)
  puts "Status: #{mandate.status}"
end
package com.myInvoicingApp;

import com.gocardless.GoCardlessClient;
import com.gocardless.resources.Mandate;
import com.myInvoicingApp.User;

public class CancelMandate {
    public static void main(User currentUser, String mandateID) {
        GoCardlessClient client = GoCardlessClient
            .newBuilder(CurrentUser.gocardlessAccessToken)
            .withEnvironment(GoCardlessClient.Environment.SANDBOX)
            .build();

        Mandate mandate = client.mandates().cancel(mandateID).execute();
        System.out.printf("Status: %s%n", mandate.getStatus());
    }

}
const mandateId = "MD0000123ABC";
console.log(`Cancelling mandate: ${mandateId}`);

const mandate = await client.mandate.cancel(mandateId);
console.log(`Status: ${mandate.status}`);
const string mandateId = "MD0000123ABC";
Console.WriteLine($"Cancelling mandate: {mandateId}");
 
var cancelResponse = await client.Mandates.CancelAsync(mandateId);
var mandate = cancelResponse.Mandate;
Console.WriteLine($"Status: {mandate.Status}");
ctx := context.TODO()
mandateCancelParams := gocardless.MandateCancelParams{}
mandate, err := client.Mandates().Cancel(ctx, mandateId, mandateCancelParams)
fmt.Printf("Status: %s%n", mandate.Status)

Only cancel a mandate if the customer has explicitly requested it.

Mandate failed#

If the mandate cannot be set up; for example because the bank account doesn't support Direct Debit or has been closed, you'll receive:

{
  "events": {
    "id": "EV124",
    "resource_type": "mandates",
    "action": "failed",
    "links": {
      "mandate": "MD123"
    },
    "details": {
      "origin": "bank",
      "cause": "bank_account_closed",
      "description": "The bank account for this mandate has been closed."
    }
  }
}

When you receive this event you should:

  • Update the mandate status in your database
  • Notify the customer and prompt them to set up a new mandate with a different bank account

Use details.cause to tailor your error message, the same cause values apply as for cancellations above

Mandate expired#

Mandates can expire after a period of inactivity under certain schemes. Treat an expired event the same way as a cancelled event, update your records and prompt the customer to set up a new mandate if needed.

{
  "events": {
    "resource_type": "mandates",
    "action": "expired",
    "links": {
      "mandate": "MD123"
    }
  }
}

Mandate replaced#

When a merchant enables the Merchant name on bank statement add-on, GoCardless moves their mandates from the GoCardless shared scheme identifier to the merchant's own identifier. For BACS mandates this results in a new mandate ID.

You'll receive:

{
  "events": {
    "resource_type": "mandates",
    "action": "replaced",
    "links": {
      "mandate": "MD123",
      "new_mandate": "MD456"
    }
  }
}

This requires immediate action. If you attempt to collect a payment against the old mandate ID you'll receive an invalid_state error with reason mandate_replaced. Update the mandate ID in your database to links.new_mandate as soon as you receive this event.

Mandate moved to another creditor#

GoCardless can move a mandate from one creditor to another. A move creates a new mandate on the destination creditor and cancels the old one. GoCardless doesn't send the customer a notification for the move.

You'll receive a cancelled event for the old mandate. The event has details.origin set to gocardless and details.cause set to other. The event links identify the new mandate and the organisation that owns it:

{
  "events": {
    "resource_type": "mandates",
    "action": "cancelled",
    "details": {
      "origin": "gocardless",
      "cause": "other"
    },
    "links": {
      "mandate": "MD123",
      "new_mandate": "MD456",
      "new_organisation": "OR456"
    }
  }
}
LinkMeaning
new_mandateThe ID of the new mandate on the destination creditor
new_organisationThe ID of the organisation that owns the new mandate

Update the mandate ID in your database to links.new_mandate as soon as you receive this event. Don't treat the event as a customer cancellation.

Find the destination organisation#

Compare links.new_organisation with the organisation the event was sent for:

  • Same organisation: the mandate moved between creditors of the same organisation. The customer and customer bank account are reused, so only the mandate ID changes.
  • Different organisation: the mandate now belongs to another organisation, with new customer and customer bank account records. If you're a partner acting for connected merchants, check that you have an active connection to links.new_organisation. If you do, use that organisation's access token to retrieve or update the new mandate. If you don't, you can't access the new mandate.

Metadata#

The new mandate keeps the metadata of the old mandate. If you match records using your own metadata values, they're still present on the new mandate. Use links.new_mandate to find the new mandate ID, and don't assume the mandate ID is unchanged.

Full list of mandate events#

For all possible mandate events and their details see the API reference - mandate event types.

What's next?#