Direct Debit

Direct Debit

Collect payments directly from a UK customer's bank account using Bacs Direct Debit. The customer authorizes you once by setting up a mandate — ideal for recurring bills, subscriptions, and invoices where you don't want the customer to re-enter details each time.

Setting up a mandate saves a payment method. A Direct Debit mandate is a saved payment method on the customer — once it's set up, you use it exactly like a saved card: charge it server-side with no customer interaction, attach it to a subscription for recurring billing, or collect an invoice with it. The difference is how the payment method is captured (a bank-account mandate rather than card details) and that collections settle over the Bacs cycle rather than instantly.

🚧

Beta

Direct Debit is in limited availability. It must be enabled for your account, and a Bacs payment rail (Service User Number and scheme credentials) has to be provisioned before you can capture mandates. Contact us to get set up. Refunding a Direct Debit charge is not yet available.

How Direct Debit works

There are two stages, and both are asynchronous — Bacs is a batch scheme, so outcomes arrive over several working days rather than instantly.

  1. Set up the mandate (save the payment method) — the customer authorizes you to debit their account. The mandate is submitted to Bacs, which confirms it over the following few working days (typically around 3–5). You can start using the saved payment method straight away — you don't have to wait for that confirmation.
  2. Use the saved payment method — charge the customer, attach the payment method to a subscription, or collect an invoice. Funds are pulled from the customer's bank account and each collection settles a few working days after you submit it.

Because nothing completes synchronously, you must rely on webhooks to learn the outcome of a mandate setup or a collection — the API response to your request only tells you the operation was accepted, not that it succeeded. See Webhooks.

Before you start

  • Your account must have a Bacs rail provisioned. This carries your Service User Number (SUN) and the originator identity that appears on your customers' bank statements. This is configured by Synapto — see the beta note above.

  • Direct Debit is UK-only. Both the customer and their bank account must be in the UK (GBP).

  • The customer needs complete contact details. Bacs mandate setup sources the payer's details from the Customer resource, so before setting up a mandate the customer must have:

    • email
    • phone (E.164 format, e.g. +447700900000)
    • a full postal address — at least address_lines[0] and a valid UK postal_code; country_code must be GB (or unset)

    Set these on the customer when you create it. If any are missing or malformed, mandate setup fails with a clear error naming the field.

Stage 1 — Set up a mandate

Setting up a mandate captures the customer's bank details and their acceptance of the Direct Debit Guarantee, then submits the authorization to Bacs. Synapto records the mandate-of-record (including the consent audit trail) for you.

You always create a customer first, then start the setup one of two ways:

  • Checkout session in SETUP mode — Synapto hosts the whole thing. You get a URL, redirect the customer, and they complete the mandate on a Synapto-hosted page. No frontend work. Recommended for most integrations.
  • Your own page with the JS SDK — you create a BACS_DD setup intent, get back a client_secret, and collect the mandate on your own page using the SDK's payment element (the same flow as saving a card). Use this when you want the mandate form inside your own UI, or when you need to supply your own Direct Debit reference (dd_reference) on a PLATFORM_PROVIDED rail.

Both produce the same result — a mandate attached to the customer as a reusable payment_method.

1. Create a customer (server)

API reference

Populate the fields Bacs requires (see Before you start):

curl -X POST https://api.synaptopay.com/v1/accounts/acct_YOUR_ACCOUNT/customers \
  -H "Authorization: Api-Key $API_KEY" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "full_name": "Pat Customer",
      "email": "[email protected]",
      "phone": "+447700900000",
      "address": {
        "address_lines": ["1 High Street"],
        "locality": "London",
        "postal_code": "SW1A 1AA",
        "country_code": "GB"
      }
    }
  }'

2, Option A — Create a checkout session in SETUP mode (server)

API reference

SETUP mode saves a payment method without charging, so omit amount and currency. A customer is required — the mandate is attached to it. On the hosted page the customer picks Direct Debit from the payment methods enabled on your account.

curl -X POST https://api.synaptopay.com/v1/accounts/acct_YOUR_ACCOUNT/checkout-sessions \
  -H "Authorization: Api-Key $API_KEY" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "SETUP",
    "customer": "accounts/acct_YOUR_ACCOUNT/customers/cus_CUSTOMER_ID",
    "success_url": "https://example.com/mandate-complete",
    "cancel_url": "https://example.com/account"
  }'
{
  "name": "accounts/acct_YOUR_ACCOUNT/checkout-sessions/cs_SESSION_ID",
  "mode": "SETUP",
  "state": "OPEN",
  "customer": "accounts/acct_YOUR_ACCOUNT/customers/cus_CUSTOMER_ID",
  "setup_intent": "accounts/acct_YOUR_ACCOUNT/setup-intents/seti_SETUP_INTENT_ID",
  "url": "https://pay.synaptopay.com/checkout?s_checkout_session=cs_SESSION_ID",
  "paid": false,
  "success_url": "https://example.com/mandate-complete",
  "cancel_url": "https://example.com/account",
  "expire_time": "2025-11-18T16:00:00Z",
  "create_time": "2025-11-17T16:00:00Z"
}

Redirect the customer to the url from the response. They enter their sort code and account number and accept the Direct Debit Guarantee; Synapto validates the bank details, submits the mandate to Bacs, and records the consent audit trail. When they finish, they're returned to your success_url. Skip to step 3.

2, Option B — Collect the mandate on your own page (server + frontend)

Instead of the hosted page, you can create a setup intent and collect the mandate on your own page with the JS SDK — the same paymentElement flow used to save a card, which renders a Direct Debit mandate form (bank details + Direct Debit Guarantee) when the setup intent is for a Bacs rail.

Create the setup intent (server)API reference. Set payment_method_type: BACS_DD; return_url is then required, and the customer must already carry the Bacs-required contact fields. On a PLATFORM_PROVIDED rail, supply your own statement reference with dd_reference (6–18 alphanumeric characters); on a SYNAPTO_ASSIGNED rail, omit it and Synapto allocates one.

curl -X POST https://api.synaptopay.com/v1/accounts/acct_YOUR_ACCOUNT/setup-intents \
  -H "Authorization: Api-Key $API_KEY" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": "accounts/acct_YOUR_ACCOUNT/customers/cus_CUSTOMER_ID",
    "payment_method_type": "BACS_DD",
    "return_url": "https://example.com/mandate-complete"
  }'
{
  "name": "accounts/acct_YOUR_ACCOUNT/setup-intents/seti_SETUP_INTENT_ID",
  "client_secret": "seti_SETUP_INTENT_ID_secret_SECRET",
  "customer": "accounts/acct_YOUR_ACCOUNT/customers/cus_CUSTOMER_ID",
  "state": "REQUIRES_PAYMENT_METHOD",
  "return_url": "https://example.com/mandate-complete"
}

Collect and confirm the mandate (frontend) — return the client_secret to your page, mount the payment element, and call confirmSetup(). The element collects the customer's bank details and Direct Debit Guarantee consent; their raw details never touch your servers.

<div id="mandate-container"></div>
<button id="setup-btn">Set up Direct Debit</button>

<script src="https://js.synaptopay.com/synapto.js"></script>
<script>
  const syn = Synapto("pk_YOUR_PUBLISHABLE_KEY");

  fetch("/api/create-setup-intent", { method: "POST" })
    .then((res) => res.json())
    .then(({ clientSecret }) => {
      const paymentElement = syn.paymentElement(clientSecret);
      paymentElement.mount("#mandate-container");

      document.getElementById("setup-btn").addEventListener("click", async () => {
        const { setupIntent, error } = await paymentElement.confirmSetup();
        if (error) {
          // Show the error — the customer can correct their details and retry
        } else {
          // For Direct Debit, setupIntent.state is PROCESSING — the mandate has been
          // submitted to Bacs and is awaiting scheme confirmation. Confirm the outcome
          // from webhooks (next step), not from this response.
          window.location.href = "/mandate-complete";
        }
      });
    });
</script>

Unlike a card (which returns SUCCEEDED on confirmSetup), a Direct Debit setup returns PROCESSING — the mandate has been submitted but not yet confirmed by the scheme.

3. Get notified of the mandate (server)

When the customer completes setup, the setup intent moves to PROCESSING and the mandate is submitted to Bacs. Confirmation from the scheme takes a few working days, so you find out the result by webhook (see Webhooks). Key off the Direct Debit mandate lifecycle — two events matter:

EventWhat to do
dd_mandate.activeThe mandate is confirmed and ready to charge. The payload is the mandate, including its payment_method (pm_...) — save that against your customer. This also re-fires if a suspended mandate later recovers.
dd_mandate.failedBacs rejected the mandate; it can't be used. Prompt the customer to set it up again.

Prefer the dd_mandate.* events: they're the authoritative Direct Debit lifecycle and fire however the mandate was set up. setup_intent.succeeded fires at the same moment as dd_mandate.active and carries the same payment_method, so reach for it only if you're already handling setup intents for cards. The rest are progress information you can ignore: checkout_session.setup_succeeded / setup_intent.processing (the customer finished the form) and dd_mandate.created (mandate submitted). See Webhook events for the full list.

📘

You can start charging right away

The payment_method (pm_...) is created as soon as the customer submits the form — it's on the setup intent from the PROCESSING state onward, so you don't have to wait for dd_mandate.active to start collecting. A collection created while the mandate is still PENDING_ACTIVATION is held and taken as soon as the scheme lodges the mandate. You only need to handle dd_mandate.failed in case setup is ultimately rejected.

Mandate lifecycle

StatusMeaning
PENDING_ACTIVATIONSubmitted to Bacs; awaiting scheme confirmation (typically around 3–5 working days). Collections can already be created — they're held and taken once the scheme lodges the mandate.
ACTIVEConfirmed by the scheme.
SUSPENDEDTemporarily un-collectable but potentially recoverable (certain scheme reason codes).
CANCELLEDTerminal. Cancelled by you or the scheme after it was live.
FAILEDTerminal. The scheme rejected the mandate during setup; it never went live.

Stage 2 — Take a payment

Collect a payment exactly as you would with a saved card: create a payment intent with the mandate's payment_method and confirm: true. No customer interaction is needed. You can do this as soon as the payment method exists — you don't have to wait for the mandate to be confirmed active; a collection created against a still-PENDING_ACTIVATION mandate is held and taken once the scheme lodges it.

API reference

curl -X POST https://api.synaptopay.com/v1/accounts/acct_YOUR_ACCOUNT/payment-intents \
  -H "Authorization: Api-Key $API_KEY" \
  -H "Idempotency-Key: YOUR_UNIQUE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "10000",
    "currency": "GBP",
    "customer": "accounts/acct_YOUR_ACCOUNT/customers/cus_CUSTOMER_ID",
    "payment_method": "accounts/acct_YOUR_ACCOUNT/payment-methods/pm_PAYMENT_METHOD_ID",
    "confirm": true
  }'

Unlike a card payment, the response comes back in PROCESSING — the collection has been accepted, not completed:

{
  "name": "accounts/acct_YOUR_ACCOUNT/payment-intents/pi_PAYMENT_INTENT_ID",
  "amount": "10000",
  "currency": "GBP",
  "state": "PROCESSING",
  "customer": "accounts/acct_YOUR_ACCOUNT/customers/cus_CUSTOMER_ID",
  "payment_method": "accounts/acct_YOUR_ACCOUNT/payment-methods/pm_PAYMENT_METHOD_ID",
  "latest_charge": "accounts/acct_YOUR_ACCOUNT/charges/ch_CHARGE_ID"
}

Bacs requires the payer to be given advance notice before each collection — Synapto emails the customer this notice automatically, which is one reason a collection settles several working days after you submit it, rather than immediately.

Collection outcomes

A Direct Debit charge moves through the Bacs cycle over several working days. Track it with webhooks — do not treat the PROCESSING response as success:

EventMeaning
charge.takenThe collection has reached its payment day — funds are due to be taken from the customer's bank account. This is not confirmation of payment: the Direct Debit can still bounce (e.g. insufficient funds), in which case the money is never taken and you'll get a charge.failed.
payment_intent.succeededThe collection cleared and the funds are settled. This is the terminal success signal.
charge.failedThe collection failed — e.g. a Bacs ARUDD return (insufficient funds, account closed, payer disputed). The reason is on last_payment_error.

Because Bacs returns can arrive after the payment day, a charge.failed can follow a charge.taken — a debit that looked due can still come back unpaid. Treat a payment as complete only on payment_intent.succeeded.

Recurring payments

Direct Debit is well suited to recurring billing. Use the mandate's payment_method as the default payment method on a subscription — Synapto submits each renewal collection for you, allowing for the Bacs cycle so the funds land by the billing date. As with one-off collections, the first renewal doesn't have to wait for the mandate to be confirmed active. Renewal outcomes surface through the same charge.taken / payment_intent.succeeded / charge.failed events.

Bacs requires payers to be told about payments in advance, so Synapto emails the customer an advance notice of their upcoming payments on your behalf — for a subscription, a single covering notice describes the recurring schedule (amount and cadence) rather than one email per collection. You don't need to send these yourself.

Webhook events

Configure a webhook endpoint (see Webhooks) to receive Direct Debit events. The mandate- and charge-level events are specific to Direct Debit; the setup-intent and payment-intent events are shared with other payment methods.

EventFires when
dd_mandate.createdA mandate is submitted to Bacs (pending confirmation).
dd_mandate.activeThe scheme confirms the mandate — collections can begin.
dd_mandate.failedThe scheme rejects the mandate during setup (never went live).
dd_mandate.suspendedThe mandate becomes temporarily un-collectable (recoverable scheme reason).
dd_mandate.canceledThe mandate is cancelled (by you or the scheme).
setup_intent.processingMandate setup was submitted and is awaiting scheme confirmation.
setup_intent.succeededMandate setup completed; payload carries the payment_method.
setup_intent.canceledMandate setup was rejected or cancelled before activation.
checkout_session.setup_succeededThe customer completed a SETUP-mode checkout session.
charge.takenA collection reached its payment day; funds are due from the payer but can still bounce (not confirmation of payment).
payment_intent.succeededA collection cleared and settled successfully (terminal success).
charge.failedA collection failed — e.g. the Direct Debit bounced or was returned.

Testing in sandbox

In the sandbox environment, Direct Debit runs against a simulated Bacs scheme — no funds move and nothing is sent to the real scheme. There are two ways to exercise the flow without waiting out the real Bacs cycle:

  • Move time forward with a simulated clock — for the happy path. Because mandate activation and a collection clearing are inferred as time passes (the scheme confirms the mandate, the ARUDD window closes with no return), the way to drive them in sandbox is to fast-forward time. Create the customer with a simulated clock and advance it: the mandate moves to ACTIVE, collections settle, and the resulting webhooks (dd_mandate.active, charge.taken, payment_intent.succeeded) fire from genuine simulated state. The simulated clock also drives subscription renewals, so it's documented separately.
  • Trigger a failure on demand — for the unhappy paths. The sandbox-only endpoints below inject a specific scheme outcome (a failed collection, a suspended or rejected mandate) without moving time. These are unavailable in production.

Simulate a failed collection

Simulates a Bacs ARUDD return on a submitted charge (in PENDING or TAKEN), flipping it to FAILED and emitting charge.failed.

curl -X POST https://api.synaptopay-sandbox.com/v1/accounts/acct_YOUR_ACCOUNT/charges/ch_CHARGE_ID:trigger-collection-failure \
  -H "Authorization: Api-Key $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "arudd_reason_code": "1" }'

Common ARUDD reason codes: 0 refer to payer, 1 instruction cancelled, 2 payer deceased, 3 account transferred, 5 no account, 6 no instruction, B account closed. The code surfaces on the resulting charge.failed event as last_payment_error.code.

Simulate a mandate suspension

Simulates an ADDACS scheme event on a mandate.

curl -X POST https://api.synaptopay-sandbox.com/v1/accounts/acct_YOUR_ACCOUNT/mandates/man_MANDATE_ID:trigger-suspension \
  -H "Authorization: Api-Key $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "addacs_reason_code": "B" }'
  • On an ACTIVE mandate, this moves it to SUSPENDED.
  • On a PENDING_ACTIVATION mandate, this drives the setup-rejection path: the mandate moves to FAILED and the linked setup intent is canceled with reason FAILED_SETUP.

Accepted ADDACS codes include 0, 1, 2, 3, 5, 6, 7, B, C, D, E, F, G, H, I, K, L, M, N, O, P, Q (e.g. B account closed).

Simulate a mandate reinstatement

Simulates a positive ADDACS reinstatement, moving a SUSPENDED mandate back to ACTIVE and emitting dd_mandate.active.

curl -X POST https://api.synaptopay-sandbox.com/v1/accounts/acct_YOUR_ACCOUNT/mandates/man_MANDATE_ID:trigger-reinstatement \
  -H "Authorization: Api-Key $API_KEY"

Did this page help you?