GoCardlessDeveloper Docs
Create a sandbox account

One-off Direct Debit#

View as Markdown

One-off Direct Debits allow you to collect fixed or variable amounts from your customers on a flexible schedule. The customer sets up a Direct Debit mandate once; after that, you can collect payments of any amount at any time without requiring re-authorisation.

Best for: Usage-based billing, professional services with variable charges, invoice payments.

One-off Direct Debit vs Pay By Bank#

One-off via Direct DebitPay By Bank
Mandate requiredYesNo
Customer authorisationOnce (mandate setup)Per payment
Confirmation speed2-5 business daysMinutes
Re-use for future paymentsYes, collect again without re-authorisationNo, new authorisation each time
ProtectionScheme-specificNone
Customer experienceOne-time setup, then hands-offThe bank app redirects each time

For time-sensitive payments, consider Pay By Bank, which confirms within seconds. If you need immediate confirmation for the first payment but also want a mandate for future charges, use Instant Payment + DD Setup instead.

How it works#

  1. The customer sets up a mandate via your chosen integration
  2. You create a payment against the mandate via the API
  3. GoCardless collects the payment (typically 2-5 business days)
  4. You receive a webhook confirming the payment status
  5. Funds are included in your next payout

Key differences from recurring payments: You control when and how much to charge. There's no automatic schedule; you create each payment individually via the API.

Step-by-step guide#

Create a billing request#

curl -X POST https://api.gocardless.com/billing_requests \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "billing_requests": {
      "mandate_request": {
        "scheme": "bacs"
      }
    }
  }'
<?php
require 'vendor/autoload.php';

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

$response = $client->billingRequests()->create([
  'params' => [
    'mandate_request' => [
      'scheme' => 'bacs',
    ],
  ],
]);
import os
import gocardless_pro

client = gocardless_pro.Client(
    access_token=os.environ['GC_ACCESS_TOKEN'],
    environment='sandbox'
)

response = client.billing_requests.create(
    params={
        'mandate_request': {
            'scheme': 'bacs',
        }
    }
)
require 'gocardless_pro'

client = GoCardlessPro::Client.new(
  access_token: ENV['GC_ACCESS_TOKEN'],
  environment: :sandbox
)

response = client.billing_requests.create(
  params: {
    mandate_request: {
      scheme: 'bacs'
    }
  }
)
import com.gocardless.GoCardlessClient;

GoCardlessClient client = GoCardlessClient
    .newBuilder(System.getenv("GC_ACCESS_TOKEN"))
    .withEnvironment(GoCardlessClient.Environment.SANDBOX)
    .build();

BillingRequestCreateResponse response = client.billingRequests()
    .create()
    .withMandateRequestScheme("bacs")
    .execute();
const gocardless = require("gocardless-nodejs");
const constants = require("gocardless-nodejs/constants");

const client = gocardless(process.env.GC_ACCESS_TOKEN, constants.Environments.Sandbox);

const response = await client.billingRequests.create({
  mandate_request: {
    scheme: "bacs",
  },
});
using GoCardless;

var client = GoCardlessClient.Create(
    Environment.GetEnvironmentVariable("GC_ACCESS_TOKEN"),
    GoCardlessClient.Environment.SANDBOX
);

var response = await client.BillingRequests.CreateAsync(
    new BillingRequestCreateRequest
    {
        MandateRequest = new BillingRequestCreateRequest.BillingRequestMandateRequest
        {
            Scheme = "bacs"
        }
    }
);
package main

import (
    "context"
    "fmt"
    "os"

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

func main() {
    opts := gocardless.WithEndpoint(gocardless.SandboxEndpoint)
    client, err := gocardless.New(os.Getenv("GC_ACCESS_TOKEN"), opts)
    if err != nil {
        panic(err)
    }

    params := gocardless.BillingRequestCreateParams{
        MandateRequest: &gocardless.BillingRequestCreateParamsMandateRequest{
            Scheme: "bacs",
        },
    }

    result, err := client.BillingRequests.Create(context.TODO(), params)
    if err != nil {
        panic(err)
    }
    fmt.Println(result)
}

Response:

{
  "billing_requests": {
    "id": "BRQ000123",
    "status": "pending",
    "mandate_request": {
      "currency": "GBP",
      "scheme": "bacs"
    }
  }
}

Confirm the mandate#

After creating the Billing Request, direct the customer to authorise the mandate. How you do this depends on your integration type:

  • Hosted Pages — Create a Billing Request Flow and redirect the customer to the authorisation_url. See Hosted Pages.
  • Custom API — Collect customer details and bank account via Billing Request actions, then confirm. See Custom Payment Pages.

Handle the outcome#

Listen for webhooks to confirm the payment status:

EventMeaning
billing_request_fulfilledCustomer authorised the mandate
mandates_createdMandate created and ready to use
mandates_activeMandate active

Collect a payment#

Retrieve the mandate ID from the fulfilled billing request:

curl https://api.gocardless.com/billing_requests/BRQ000123 \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
<?php
require 'vendor/autoload.php';

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

$response = $client->billingRequests()->get('BRQ000123');

$mandateId = $response->mandate_request->links->mandate;
import os
import gocardless_pro

client = gocardless_pro.Client(
    access_token=os.environ['GC_ACCESS_TOKEN'],
    environment='sandbox'
)

response = client.billing_requests.get('BRQ000123')

mandate_id = response.mandate_request.links.mandate
require 'gocardless_pro'

client = GoCardlessPro::Client.new(
  access_token: ENV['GC_ACCESS_TOKEN'],
  environment: :sandbox
)

response = client.billing_requests.get('BRQ000123')

mandate_id = response.mandate_request.links.mandate
import com.gocardless.GoCardlessClient;

GoCardlessClient client = GoCardlessClient
    .newBuilder(System.getenv("GC_ACCESS_TOKEN"))
    .withEnvironment(GoCardlessClient.Environment.SANDBOX)
    .build();

BillingRequest billingRequest = client.billingRequests()
    .get("BRQ000123")
    .execute();

String mandateId = billingRequest.getMandateRequest().getLinks().getMandate();
const gocardless = require("gocardless-nodejs");
const constants = require("gocardless-nodejs/constants");

const client = gocardless(process.env.GC_ACCESS_TOKEN, constants.Environments.Sandbox);

const billingRequest = await client.billingRequests.find("BRQ000123");

const mandateId = billingRequest.mandate_request.links.mandate;
using GoCardless;

var client = GoCardlessClient.Create(
    Environment.GetEnvironmentVariable("GC_ACCESS_TOKEN"),
    GoCardlessClient.Environment.SANDBOX
);

var billingRequest = await client.BillingRequests.GetAsync("BRQ000123");

var mandateId = billingRequest.MandateRequest.Links.Mandate;
package main

import (
    "context"
    "fmt"
    "os"

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

func main() {
    opts := gocardless.WithEndpoint(gocardless.SandboxEndpoint)
    client, err := gocardless.New(os.Getenv("GC_ACCESS_TOKEN"), opts)
    if err != nil {
        panic(err)
    }

    billingRequest, err := client.BillingRequests.Get(context.TODO(), "BRQ000123")
    if err != nil {
        panic(err)
    }

    mandateId := billingRequest.MandateRequest.Links.Mandate
    fmt.Println(mandateId)
}

Response:

{
  "billing_requests": {
    "id": "BRQ000123",
    "status": "fulfilled",
    "mandate_request": {
      "scheme": "bacs",
      "links": {
        "mandate": "MD123"
      }
    }
  }
}

Then collect whenever you need to:

curl -X POST https://api.gocardless.com/payments \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payments": {
      "amount": 5000,
      "currency": "GBP",
      "description": "Order #1234",
      "links": {
        "mandate": "MD123"
      }
    }
  }'
$response = $client->payments()->create([
    'params' => [
        'amount'      => 5000,
        'currency'    => 'GBP',
        'description' => 'Order #1234',
        'links'       => [
            'mandate' => 'MD123',
        ],
    ],
]);
response = client.payments.create(
    params={
        'amount':      5000,
        'currency':    'GBP',
        'description': 'Order #1234',
        'links': {
            'mandate': 'MD123',
        },
    }
)
response = client.payments.create(
  params: {
    amount:      5000,
    currency:    'GBP',
    description: 'Order #1234',
    links: {
      mandate: 'MD123'
    }
  }
)
Payment payment = client.payments()
    .create()
    .withAmount(5000)
    .withCurrency("GBP")
    .withDescription("Order #1234")
    .withLinksMandate("MD123")
    .execute();
const payment = await client.payments.create({
  amount: 5000,
  currency: "GBP",
  description: "Order #1234",
  links: {
    mandate: "MD123",
  },
});
var payment = await client.Payments.CreateAsync(
    new PaymentCreateRequest
    {
        Amount      = 5000,
        Currency    = "GBP",
        Description = "Order #1234",
        Links = new PaymentCreateRequest.PaymentLinks
        {
            Mandate = "MD123"
        }
    }
);
params := gocardless.PaymentCreateParams{
    Amount:      5000,
    Currency:    "GBP",
    Description: "Order #1234",
    Links: gocardless.PaymentCreateParamsLinks{
        Mandate: "MD123",
    },
}

payment, err := client.Payments.Create(context.TODO(), params)

What's next?#