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.
BetaDirect 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.
- 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.
- 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:
emailphone(E.164 format, e.g.+447700900000)- a full postal
address— at leastaddress_lines[0]and a valid UKpostal_code;country_codemust beGB(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
SETUPmode — 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_DDsetup intent, get back aclient_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 aPLATFORM_PROVIDEDrail.
Both produce the same result — a mandate attached to the customer as a reusable payment_method.
1. Create a customer (server)
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)
SETUP mode (server)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:
| Event | What to do |
|---|---|
dd_mandate.active | The 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.failed | Bacs 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 awayThe
payment_method(pm_...) is created as soon as the customer submits the form — it's on the setup intent from thePROCESSINGstate onward, so you don't have to wait fordd_mandate.activeto start collecting. A collection created while the mandate is stillPENDING_ACTIVATIONis held and taken as soon as the scheme lodges the mandate. You only need to handledd_mandate.failedin case setup is ultimately rejected.
Mandate lifecycle
| Status | Meaning |
|---|---|
PENDING_ACTIVATION | Submitted 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. |
ACTIVE | Confirmed by the scheme. |
SUSPENDED | Temporarily un-collectable but potentially recoverable (certain scheme reason codes). |
CANCELLED | Terminal. Cancelled by you or the scheme after it was live. |
FAILED | Terminal. 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.
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:
| Event | Meaning |
|---|---|
charge.taken | The 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.succeeded | The collection cleared and the funds are settled. This is the terminal success signal. |
charge.failed | The 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.
| Event | Fires when |
|---|---|
dd_mandate.created | A mandate is submitted to Bacs (pending confirmation). |
dd_mandate.active | The scheme confirms the mandate — collections can begin. |
dd_mandate.failed | The scheme rejects the mandate during setup (never went live). |
dd_mandate.suspended | The mandate becomes temporarily un-collectable (recoverable scheme reason). |
dd_mandate.canceled | The mandate is cancelled (by you or the scheme). |
setup_intent.processing | Mandate setup was submitted and is awaiting scheme confirmation. |
setup_intent.succeeded | Mandate setup completed; payload carries the payment_method. |
setup_intent.canceled | Mandate setup was rejected or cancelled before activation. |
checkout_session.setup_succeeded | The customer completed a SETUP-mode checkout session. |
charge.taken | A collection reached its payment day; funds are due from the payer but can still bounce (not confirmation of payment). |
payment_intent.succeeded | A collection cleared and settled successfully (terminal success). |
charge.failed | A 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
ACTIVEmandate, this moves it toSUSPENDED. - On a
PENDING_ACTIVATIONmandate, this drives the setup-rejection path: the mandate moves toFAILEDand the linked setup intent is canceled with reasonFAILED_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"Updated 19 days ago