Sandbox
Help

Accept payments via SEPA Direct Debit - Web

Revolut's SEPA Direct Debit widget allows you to accept payments directly from your customers' EUR-denominated bank accounts, offering a seamless checkout experience for subscriptions, recurring or one-time payments across the SEPA region.

In this tutorial, we'll guide you through integrating the SEPA Direct Debit widget, powered by the Revolut Checkout Widget. Customers can securely authorise a direct debit mandate and submit payments directly from their bank account within your website, simplifying the payment process for European customers.

SEPA Direct Debit on Web

Limitations

Before implementing SEPA Direct Debit, be aware of the following:

  • Not enabled by default: SEPA Direct Debit must be enabled on your merchant account. Contact your account executive to request enablement.
  • Country availability: SEPA Direct Debit is available in 34 of the 36 SEPA countries - all except Portugal and Ireland. See the full country list.
  • EUR only: All SEPA Direct Debit payments are denominated in EUR.
  • No sandbox: SEPA Direct Debit is not available in the sandbox environment. Testing requires a production merchant account with SEPA Direct Debits enabled.
  • Creditor ID: You need a valid SEPA Creditor ID, which must be shared with Revolut.

What is SEPA Direct Debit?

SEPA Direct Debit is a European payment scheme that lets merchants collect payments directly from customers' EUR-denominated bank accounts. Revolut supports the SEPA Direct Debit Core scheme, which covers both personal and business bank accounts across the 36-country SEPA region.

SEPA Direct Debit is mandate-based: the customer authorises a direct debit mandate once, and you can collect payments on a schedule without requiring re-authorisation for each payment. This makes it a popular choice for subscriptions, recurring billing, and one-time payments in the European market.

Key characteristics of the scheme:

  • EUR only: all SEPA Direct Debit payments are denominated in EUR.
  • Asynchronous settlement: payments are submitted to the SEPA scheme and settle over several business days (up to 5).
  • Mandate-driven: each payment is backed by a signed mandate. You are responsible for mandate management and pre-debit notifications as required by SEPA rules.

How it works

Implementing the SEPA Direct Debit widget involves these core components:

  1. Server-side: expose an endpoint on your backend that creates an order using the Merchant API: Create an order endpoint.
  2. Client-side: the SEPA Direct Debit widget collects the customer's IBAN and mandate consent, uses your endpoint to create the order, and submits the payment to the SEPA scheme.
  3. Webhook endpoint: set up webhooks to monitor payment lifecycle events. SEPA Direct Debit payments are fully asynchronous - payment outcomes can arrive up to 5 business days after submission. Webhooks are mandatory - at minimum, subscribe to ORDER_PAYMENT_AUTHORISATION_STARTED to receive mandate details for pre-notification, and ORDER_COMPLETED for final settlement confirmation.

The order and payment flow is as follows:

  1. The customer goes to the checkout page, chooses to pay via SEPA Direct Debit and clicks the payment button.
  2. The widget opens the SEPA Direct Debit pop-up and calls your createOrder function to create the order via your backend in the background.
  3. The customer enters their IBAN and provides mandate consent in the pop-up.
  4. The widget submits the payment to the SEPA scheme.
  5. The payment enters processing state - this is the final client-side state, not completed.
  6. The onSuccess callback fires - this signals that the payment has been submitted, not that it has settled.
  7. Your server receives the ORDER_PAYMENT_AUTHORISATION_STARTED webhook event.
  8. You pull the mandate reference and IBAN details from the Retrieve an order endpoint.
  9. You must send the mandate confirmation and pre-notification to the customer as required by SEPA rules. Failure to send these notifications may result in payment disputes.
  10. The payment settles asynchronously - this can take up to 5 business days.
  11. Your server receives a final webhook event: ORDER_COMPLETED (or ORDER_PAYMENT_DECLINED / ORDER_PAYMENT_FAILED / ORDER_CANCELLED if unsuccessful).

For more information about the order and payment lifecycle, see: Order and payment lifecycle.

Asynchronous settlement

Unlike card payments, the final client-side state for SEPA Direct Debit is processing, not completed. The onSuccess callback fires when the payment is submitted, not when it settles.

Key points to keep in mind:

  • Settlement can take up to 5 business days after submission.
  • Authorisation started does not guarantee payment completion - the debtor's bank can still return the payment within the refusal window.
  • Fulfil orders only after receiving the ORDER_COMPLETED webhook event.
  • If the payment fails after authorisation has started, you can retry using the saved payment method - the payment method is saved by default (isRecurringPayment defaults to true). See Step 5: Handle payment results.

Pre-notification handling

In the SDK flow, Revolut ensures that debtors agree to receive pre-notifications 2 days before payment during the widget interaction. However, you are still responsible for actually sending the mandate confirmation and pre-notification after the payment authorisation started.

After a successful mandate submission:

  1. Pull the mandate reference and IBAN details from the order or payment details.
  2. Send the mandate confirmation and pre-notification to the customer as per SEPA rules.

As part of SEPA compliance, you are responsible for sending mandate confirmations and pre-notifications to your customers. Revolut does not send these on your behalf.

For full regulatory requirements - mandate management and pre-notification content - see Regulatory responsibilities.

Implementation overview

Here is an overview of the key integration steps:

  1. Set up an endpoint for creating orders
  2. Install the SDK
  3. Initialise the SDK
  4. Configure and mount the widget
  5. Handle payment results

Before you begin

Make sure you've completed the following:

  • Apply for a Merchant account
  • Generate the API keys
  • Contacted your account executive to have SEPA Direct Debits enabled on your merchant account (not enabled by default)
  • Obtained a valid SEPA Creditor ID
  • Shared your Creditor ID with Revolut
  • Set up a process to send pre-debit notifications to customers as required by SEPA rules
  • Confirmed your target markets are supported - see the country availability list

Implement SEPA Direct Debit

The following steps walk you through the full integration: setting up your backend to create orders, installing and initialising the SDK, configuring and mounting the SEPA Direct Debit widget, and handling payment results - including the mandatory webhook pipeline for asynchronous settlement.

1. Set up an endpoint for creating orders

Before implementing the client-side widget, you must first create a dedicated endpoint on your server. This is a critical security step, as your secret API key must never be exposed on the client side.

The role of this server-side endpoint is to act as a secure bridge between your frontend and the Merchant API. When a customer initiates a payment on your website, your frontend will call this endpoint. Your endpoint is then responsible for:

  1. Receiving the checkout details (e.g., amount, currency) from the frontend request.
  2. Securely calling the Merchant API: Create an order endpoint with the received details.
  3. Receiving the order details from the Merchant API, including the public token.
  4. Passing this token back to your frontend in the response.

Later, in the client-side SDK configuration, the createOrder callback function will call this endpoint to fetch the token, which is required to initialise the checkout widget.

Send a POST request to the Create an order endpoint with the order details, including your Secret API key in the authorisation header:

Request example
POST /api/orders HTTP/1.1
Host: merchant.revolut.com
Authorization: Bearer sk_abcdef12347890_...
Content-Type: application/json

{
  "amount": 1000,
  "currency": "EUR",
  "customer": {
    "email": "customer@example.com"
  }
}

SEPA Direct Debit payments are denominated in EUR only.

ParameterDescriptionRequired
amountThe order amount in minor currency unit (e.g., cents). For €10.00, use 1000.Yes
currencyMust be EUR for SEPA Direct Debit payments.Yes
customerCustomer details. Pass email for new customers or id for existing customers. Required when isRecurringPayment is true (default) to save the payment method for MIT. Optional for one-time payments.Yes*

* Required when isRecurringPayment is true (default). Optional for one-time payments where isRecurringPayment is false.

For complete details on all available parameters, see: Merchant API: Create an order.

2. Install the SDK

Before you begin working with the SDK, ensure the Revolut Checkout Widget is installed in your project. This widget is a necessary component to create and configure the SEPA Direct Debit widget. You can install the widget via your project's package manager.

npm install @revolut/checkout
yarn add @revolut/checkout
pnpm add @revolut/checkout
bun add @revolut/checkout

Alternatively, you can add the widget to your code base by adding the embed script to your page directly. To learn more, see: Installation.

3. Initialise the SDK

Use the RevolutCheckout.payments() module's sepaDirectDebit instance with your Merchant API Public key to initialise the widget.

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

// Configuration code will go here
ParameterDescriptionTypeRequired
publicTokenYour Merchant API public keystringYes
modeAPI environment. Default: 'prod''prod' | 'sandbox'No
localeWidget language (defaults to 'auto' for automatic detection)LocaleNo

For more information about the RevolutCheckout.payments() module, see: Payments module

4. Configure and mount the widget

Prepare your checkout page by adding a button that users click to initiate the payment, then invoke the sepaDirectDebit({ ... }) instance with your configuration parameters and call the .show() method to open the SEPA Direct Debit pop-up.

4.1 Add a DOM element

First, add a button to your checkout page where you want the widget to appear. This can be a standalone button, or it can be triggered from a payment method selector - for example, the customer selects SEPA Direct Debit from a list of payment options, then clicks a pay button that triggers this button's click handler.

checkout.html
<!-- ... -->

<button id="sepa-direct-debit-button">Pay by SEPA Direct Debit</button>

<!-- ... -->

4.2 Set up the click handler

Attach a click handler to your button. For now, this is just a placeholder - you'll add the widget configuration in the next step.

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

const sepaDirectDebitButton = document.getElementById('sepa-direct-debit-button')

sepaDirectDebitButton.addEventListener('click', async () => {
  // Widget configuration will go here
})

4.3 Configure the widget

Invoke the sepaDirectDebit({ ... }) instance inside the click handler with your configuration parameters, and call .show() to open the SEPA Direct Debit pop-up.

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

const sepaDirectDebitButton = document.getElementById('sepa-direct-debit-button')

sepaDirectDebitButton.addEventListener('click', async () => {
  const instance = sepaDirectDebit({
    createOrder: async () => {
      // Call your backend here to create an order and return order.token
      const order = await yourServerSideCall()
      return { publicId: order.token }
    },
  })

  instance.show()
})
Code snippetDescription
RevolutCheckout.payments(...)Initialises the Revolut Checkout payments module and returns an object containing the sepaDirectDebit factory. Pass your publicToken and mode here.
sepaDirectDebitCreates a SEPA Direct Debit instance when called with your options. It returns a SepaDirectDebitInstance.
SepaDirectDebitInstanceThe instance returned by sepaDirectDebit. It exposes .show() to open the mandate collection pop-up and .destroy() to remove any rendered UI and clean up.
createOrderA function you define to send order details like amount, currency from your frontend to your backend. Your backend then calls Merchant API: Create an order, receives the token, and returns { publicId: order.token } to the widget, so it can start the checkout session.
Instance methodsCall .show() to open the SEPA Direct Debit pop-up. Call .destroy() to close the widget and clean up resources.

For more details about the available parameters, see: Merchant Web SDK: SEPA Direct Debit.

Additional settings

The sepaDirectDebit module of the Revolut Checkout Widget offers several options that allow for a customised checkout experience. Here are some additional settings you can leverage:

  1. Merchant-initiated transactions:

    • isRecurringPayment defaults to true, meaning the payment method is saved for the merchant after a successful payment. This enables merchant-initiated transactions (MIT) - retries and recurring charges - via the Merchant API (POST /api/payments) without requiring the customer to re-enter their IBAN. Set to false for one-time payments where the method should not be retained.
  2. Billing address:

    • Pass the billingAddress parameter to pre-fill the customer's billing address in the widget. If the address is valid (countryCode, postcode, streetLine1, city), the billing form is hidden. Accepts an Address object with fields like countryCode, region, city, streetLine1, streetLine2, and postcode.

5. Handle payment results

SEPA Direct Debit payments are processed asynchronously. The client-side callbacks provide immediate feedback to the customer, but the final payment outcome arrives via webhooks - this can take up to 5 business days.

Client-side callbacks

The widget is configured and mounted. Add the event callbacks to handle the payment results. Add onSuccess, onError, and onCancel inside the sepaDirectDebit({...}) configuration object:

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

const sepaDirectDebitButton = document.getElementById('sepa-direct-debit-button')

sepaDirectDebitButton.addEventListener('click', async () => {
  const instance = sepaDirectDebit({
    createOrder: async () => {
      // Call your backend here to create an order and return order.token
      const order = await yourServerSideCall()
      return { publicId: order.token }
    },

    onSuccess: ({ orderId }) => {
      // Payment has been submitted - not yet settled
      console.log('Payment submitted!', orderId)
      window.location.href = `/processing?orderId=${orderId}`
    },

    onError: ({ error, orderId }) => {
      console.error('Payment failed:', error.message, orderId)
      alert(`Payment failed: ${error.message}`)
    },

    onCancel: ({ orderId }) => {
      console.log('Payment cancelled', orderId)
      alert('Payment was cancelled.')
    },
  })

  instance.show()
})
CallbackDescription
onSuccessFires when the payment is submitted - not when it settles. Redirect to a "payment processing" page, not a confirmation page. Settlement can take up to 5 business days.
onErrorFires when an error occurs. Receives RevolutCheckoutError. Use it to display error messaging, log diagnostics, or prompt the user to try again.
onCancelFires when the user cancels the payment. orderId may be undefined if order creation failed. Use it to re-enable the checkout form or track abandonment.
Callbacks are not guaranteed

Widget callbacks are not guaranteed to fire due to network issues, browser closures, or ad-blockers. Always use webhooks for critical backend operations like order fulfilment.

Webhooks (mandatory)

For SEPA Direct Debit, webhooks are mandatory. Set up your backend to receive and process webhooks before you go live. SEPA Direct Debit is fully asynchronous - payment outcomes can arrive up to 5 business days after submission.

Subscribe to the following events via the Create a webhook endpoint:

Send a POST request to the Create a webhook endpoint with your webhook URL and the events you want to subscribe to:

Request example
POST /api/webhooks HTTP/1.1
Host: merchant.revolut.com
Authorization: Bearer sk_abcdef12347890_...
Content-Type: application/json

{
  "url": "https://example.com/webhooks",
  "events": [
    "ORDER_AUTHORISED",
    "ORDER_COMPLETED",
    "ORDER_PAYMENT_DECLINED",
    "ORDER_PAYMENT_FAILED",
    "ORDER_CANCELLED",
    "ORDER_FAILED",
    "ORDER_PAYMENT_AUTHORISATION_STARTED"
  ]
}
ParameterDescriptionRequired
urlYour webhook URL. Must be a valid HTTP or HTTPS URL capable of receiving POST requests.Yes
eventsArray of event types to subscribe to. See Webhook events for the full list.Yes

For detailed information on setting up webhooks, see: Use webhooks to track order and payment lifecycle.

Payment outcomes

Handle the following scenarios:

The payment has been submitted to the SEPA scheme. This is the key event for SEPA Direct Debit - it signals that the customer has provided their IBAN and mandate consent, and the debit instruction has been queued.

Webhook expected:

Webhook eventDescription
ORDER_PAYMENT_AUTHORISATION_STARTEDThe payment has been submitted to the SEPA scheme

What to do:

  1. Pull the mandate reference and IBAN details from the Retrieve an order endpoint. Check for the following fields in payments[].payment_method:

    FieldDescription
    mandate_referenceThe unique mandate reference number generated by Revolut
    debtor_iban_last_fourThe last 4 digits of the customer's IBAN
    debtor_nameThe account holder's name
  2. Send the mandate confirmation and pre-notification to the customer as per SEPA rules.

In the SDK flow, Revolut ensures that debtors agree to receive pre-notifications 2 days before payment during the widget interaction.

However, you are still responsible for actually sending the mandate confirmation and pre-notification after receiving this webhook event. Revolut does not send pre-notifications on your behalf.

Regulatory responsibilities

SEPA rules impose obligations on you before and after initiating any direct debit. You are fully responsible for compliance - Revolut collects mandate consent through the widget but does not store mandate records or send pre-notifications on your behalf.

Mandate management:

RequirementDetails
Mandatory fieldsDebtor name, IBAN, creditor identifier, mandate reference, customer consent
StorageStore mandate records securely for as long as they remain valid and for at least 14 months after the last collection
Mandate referenceRevolut generates a unique mandate reference number and returns it in the payment response. Use this reference in pre-debit notifications sent to your customers.
Audit readinessBe prepared to provide proof of mandate authorisation if challenged by the debtor's bank

Pre-notification requirements:

In the SDK flow, customers agree to receive pre-notifications 2 days before payment during the widget interaction. You must send the pre-notification at least 2 calendar days before each debit date.

Each notification must include:

Required contentDetails
Last 4 digits of IBANThe last 4 digits of the debtor's bank account number
Mandate referenceThe unique reference number of the mandate
Debit amountThe amount to be debited
Creditor identifierYour SEPA creditor identifier
Contact informationYour business contact details

For subscriptions with fixed amounts and regular dates, a single pre-notification can cover multiple future collections if the schedule is clearly communicated.

If anything in the mandate changes - amount, date, or any other terms - you must collect a new mandate from the customer. Revolut does not support mandate updates.

Examples

Explore our integration examples repository
Discover all available examples and see how different payment solutions are implemented

Example with minimal required parameters

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

const sepaDirectDebitButton = document.getElementById('sepa-direct-debit-button')

sepaDirectDebitButton.addEventListener('click', async () => {
  const instance = sepaDirectDebit({
    createOrder: async () => {
      // Call your backend here to create an order and return order.token
      const order = await yourServerSideCall()
      return { publicId: order.token }
    },

    onSuccess: ({ orderId }) => {
      console.log('Payment submitted!', orderId)
      window.location.href = `/processing?orderId=${orderId}`
    },

    onError: ({ error, orderId }) => {
      console.error('Payment failed:', error.message, orderId)
      alert(`Payment failed: ${error.message}`)
    },
  })

  instance.show()
})

Example with additional parameters

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

const sepaDirectDebitButton = document.getElementById('sepa-direct-debit-button')

sepaDirectDebitButton.addEventListener('click', async () => {
  const instance = sepaDirectDebit({
    createOrder: async () => {
      // Call your backend here to create an order and return order.token
      const order = await yourServerSideCall()
      return { publicId: order.token }
    },

    billingAddress: {
      countryCode: 'DE',
      region: 'Berlin',
      city: 'Berlin',
      streetLine1: 'Musterstraße 1',
      streetLine2: '',
      postcode: '10115',
    },

    isRecurringPayment: true, // default - saved for MIT

    onSuccess: ({ orderId }) => {
      console.log('Recurring payment submitted!', orderId)
      window.location.href = `/processing?orderId=${orderId}`
    },

    onError: ({ error, orderId }) => {
      console.error('Payment failed:', error.message, orderId)
      alert(`Payment failed: ${error.message}`)
    },

    onCancel: ({ orderId }) => {
      console.log('Payment cancelled', orderId)
      alert('Payment was cancelled.')
    },
  })

  instance.show()
})

Example: One-time payment

By default, isRecurringPayment is true and the payment method is saved for the merchant. For one-time payments where the method should not be retained, set isRecurringPayment: false:

my-app.js
import RevolutCheckout from '@revolut/checkout'

const { sepaDirectDebit } = await RevolutCheckout.payments({
  publicToken: 'pk_abcdef12347890_...', // Merchant public API key
  mode: 'prod',
})

const sepaDirectDebitButton = document.getElementById('sepa-direct-debit-button')

sepaDirectDebitButton.addEventListener('click', async () => {
  const instance = sepaDirectDebit({
    createOrder: async () => {
      const order = await yourServerSideCall()
      return { publicId: order.token }
    },

    isRecurringPayment: false, // one-time - payment method won't be saved

    onSuccess: ({ orderId }) => {
      console.log('Payment submitted!', orderId)
      window.location.href = `/processing?orderId=${orderId}`
    },

    onError: ({ error, orderId }) => {
      console.error('Payment failed:', error.message, orderId)
      alert(`Payment failed: ${error.message}`)
    },
  })

  instance.show()
})

Implementation checklist

SEPA Direct Debit is not available in the Sandbox environment. Integration testing requires a production merchant account with SEPA Direct Debits enabled.

Before deploying your SEPA Direct Debit implementation to your live website, complete the checklist below to ensure everything works as expected in the production environment.

General checks

  • SEPA Direct Debit widget pop-up opens correctly when the customer clicks the payment button.

  • Your backend creates the order successfully when the widget is triggered.

  • Widget successfully calls createOrder function to fetch and use the order token.

  • IBAN and mandate consent collection screen displays correctly in the widget.

  • Payment submission flow:

    • Customer can enter their IBAN and provide mandate consent.
    • Payment is submitted successfully and onSuccess callback fires.
    • Customer is redirected to a "payment processing" page (not a confirmation page).
  • Error handling works as expected:

    • Failed payments trigger the error callback (onError).
    • Customer sees appropriate error messaging.
    • Cancel callback (onCancel) is triggered when payment is abandoned.

Webhook verification

  • Webhook endpoint is set up to receive order and payment updates.
  • Webhook subscription created with ORDER_PAYMENT_AUTHORISATION_STARTED, ORDER_AUTHORISED, ORDER_COMPLETED, ORDER_PAYMENT_DECLINED, ORDER_PAYMENT_FAILED, ORDER_CANCELLED, and ORDER_FAILED events.
  • ORDER_COMPLETED webhook event is received when payment settles successfully.
  • Your backend only fulfils orders after receiving the ORDER_COMPLETED webhook event.
  • Mandate reference and IBAN details are retrieved from the Retrieve an order endpoint after authorisation.
  • Webhook signature verification is implemented for security.

SEPA Direct Debit specific checks

  • All orders are created with currency: "EUR" (EUR-only restriction enforced).
  • Country availability confirmed for target markets - see the country availability list.
  • Mandate confirmation and pre-notification emails are sent to the customer after ORDER_PAYMENT_AUTHORISATION_STARTED is received.
  • Pre-debit notifications include: last 4 digits of IBAN, mandate reference, debit amount, creditor identifier, and contact information.
  • Pre-notifications are sent at least 2 calendar days before each debit (SDK flow).
  • Payment methods are saved by default (isRecurringPayment defaults to true) for MIT. If one-time payments are needed, isRecurringPayment: false is set correctly.
  • Orders include customer details (email for new customers, or id for existing) when isRecurringPayment is true to save the payment method for MIT.
  • Backend logic is implemented for failed payments and retries after authorisation.
  • Mandates are stored securely for at least 14 months after the last collection.

Once your implementation passes all the checks in the production environment, you can confidently deploy it to your live website.

These checks only cover the implementation path described in this tutorial. If your application handles more features of the Merchant API, see the Implementation checklists.

Congratulations! You've successfully implemented SEPA Direct Debit and are ready to accept direct debit payments from your customers.

What's next

SEPA Direct Debit Web SDK reference
Full SDK reference for the web SDK
Order and payment lifecycle
Understand how orders and payments are processed
Using webhooks
Track the payment lifecycle with server-to-server webhooks
Decline reasons
Understand why SEPA DD payments may be declined
Refunds
Learn how to refund your orders
Order management
Explore the full capabilities of the Orders API
Rate this page