# 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](/docs/sdks/merchant-web-sdk/introduction). 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](/img/accept-payments/payment-methods/sepa-direct-debit/sepa-direct-debit.png 'SEPA Direct Debit on Web')

:::note [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](/docs/guides/merchant/accept-payments/online-payments/sepa-direct-debit/introduction#available-countries).
- **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](https://www.europeanpaymentscouncil.eu/what-we-do/epc-payment-schemes/sepa-direct-debit/sepa-direct-debit-core-rulebook-and-implementation) 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](/docs/api/merchant#create-order) endpoint.
1. **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.
1. **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.
1. The widget opens the SEPA Direct Debit pop-up and calls your `createOrder` function to [create the order](/docs/api/merchant#create-order) via your backend in the background.
1. The customer enters their IBAN and provides mandate consent in the pop-up.
1. The widget submits the payment to the SEPA scheme.
1. The payment enters **`processing`** state - this is the final client-side state, not `completed`.
1. The `onSuccess` callback fires - this signals that the payment has been **submitted**, not that it has settled.
1. Your server receives the `ORDER_PAYMENT_AUTHORISATION_STARTED` webhook event.
1. You pull the mandate reference and IBAN details from the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint.
1. 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.
1. The payment settles asynchronously - this can take up to 5 business days.
1. Your server receives a final webhook event: `ORDER_COMPLETED` (or `ORDER_PAYMENT_DECLINED` / `ORDER_PAYMENT_FAILED` / `ORDER_CANCELLED` if unsuccessful).

:::info
For more information about the order and payment lifecycle, see: [Order and payment lifecycle](/docs/guides/merchant/reference/order-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](#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](/docs/api/merchant#retrieve-order) or [payment](/docs/api/merchant#retrieve-payment-details) details.
1. Send the mandate confirmation and pre-notification to the customer as per SEPA rules.

:::warning
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.
:::

:::info
For full regulatory requirements - mandate management and pre-notification content - see [Regulatory responsibilities](#regulatory-responsibilities).
:::

### Implementation overview

Here is an overview of the key integration steps:

1. [Set up an endpoint for creating orders](#1-set-up-an-endpoint-for-creating-orders)
1. [Install the SDK](#2-install-the-sdk)
1. [Initialise the SDK](#3-initialise-the-sdk)
1. [Configure and mount the widget](#4-configure-and-mount-the-widget)
1. [Handle payment results](#5-handle-payment-results)

### Before you begin

Make sure you've completed the following:

- [ ] [Apply for a Merchant account](/docs/guides/merchant/get-started)
- [ ] [Generate the API keys](/docs/guides/merchant/get-started#generate-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](/docs/guides/merchant/accept-payments/online-payments/sepa-direct-debit/introduction#available-countries)

## 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.
1. Securely calling the [Merchant API: Create an order](/docs/api/merchant#create-order) endpoint with the received details.
1. Receiving the order details from the Merchant API, including the public `token`.
1. 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.

- ![Request]

  Send a `POST` request to the [Create an order](/docs/api/merchant#create-order) endpoint with the order details, including your **Secret API key** in the authorisation header:

  ```http [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"
    }
  }
  ```

  :::warning
  SEPA Direct Debit payments are denominated in **EUR only**.
  :::

  | Parameter | Description | Required |
  |-----------|-------------|----------|
  | `amount` | The order amount in minor currency unit (e.g., cents). For €10.00, use `1000`. | Yes |
  | `currency` | Must be `EUR` for SEPA Direct Debit payments. | Yes |
  | `customer` | Customer 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`.

- ![Response]

  Below is an example of the JSON response your endpoint will receive from the Merchant API after successfully creating an order. The crucial field to extract and return to your frontend is the `token`:

  ```json [Response example] {3}
  {
    "id": "6516e61c-d279-a454-a837-bc52ce55ed49",
    "token": "0adc0e3c-ab44-4f33-bcc0-534ded7354ce",
    "type": "payment",
    "state": "pending",
    "created_at": "2023-09-29T14:58:36.079398Z",
    "updated_at": "2023-09-29T14:58:36.079398Z",
    "amount": 1000,
    "currency": "EUR",
    "outstanding_amount": 1000,
    "capture_mode": "automatic",
    "checkout_url": "https://checkout.revolut.com/payment-link/0adc0e3c-ab44-4f33-bcc0-534ded7354ce",
    "enforce_challenge": "automatic"
  }
  ```

:::info
For complete details on all available parameters, see: [Merchant API: Create an order](/docs/api/merchant#create-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.

```install
npm install @revolut/checkout
```

:::tip
Alternatively, you can add the widget to your code base by adding the embed script to your page directly. To learn more, see: [Installation](/docs/sdks/merchant-web-sdk/get-started#installation).
:::

### 3. Initialise the SDK

Use the `RevolutCheckout.payments()` module's `sepaDirectDebit` instance with your [Merchant API Public key](https://business.revolut.com/settings/apis?tab=merchant-api) to initialise the widget.

- ![With async await]

  ```js [my-app.js] {1,3-6}
  import RevolutCheckout from '@revolut/checkout'

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

  // Configuration code will go here
  ```

- ![Without async await]

  ```js [my-app.js] {1,3-6}
  import RevolutCheckout from '@revolut/checkout'

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    // Configuration code will go here
  })
  ```

| Parameter     | Description                        | Type                  | Required |
| ------------- | ---------------------------------- | --------------------- | -------- |
| `publicToken` | Your Merchant API public key       | `string`              | Yes      |
| `mode`        | API environment. Default: `'prod'` | `'prod' \| 'sandbox'` | No       |
| `locale`      | Widget language (defaults to `'auto'` for automatic detection) | [`Locale`](/docs/sdks/merchant-web-sdk/types/locale) | No |

:::info
For more information about the `RevolutCheckout.payments()` module, see: [Payments module](/docs/sdks/merchant-web-sdk/initialisation/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.

```html [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](#43-configure-the-widget).

- ![With async await]

  ```js [my-app.js] {8,10-12}
  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
  })
  ```

- ![Without async await]

  ```js [my-app.js] {3,9-11}
  import RevolutCheckout from '@revolut/checkout'

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

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    sepaDirectDebitButton.addEventListener('click', () => {
      // 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.

- ![With async await]

  ```js [my-app.js] {10-16,18}
  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()
  })
  ```

- ![Without async await]

  ```js [my-app.js] {9-15,17}
  import RevolutCheckout from '@revolut/checkout'

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

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    sepaDirectDebitButton.addEventListener('click', () => {
      const instance = sepaDirectDebit({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ publicId: order.token }))
        },
      })

      instance.show()
    })
  })
  ```

| Code snippet | Description |
| ------------ | ----------- |
| `RevolutCheckout.payments(...)` | Initialises the Revolut Checkout payments module and returns an object containing the `sepaDirectDebit` factory. Pass your `publicToken` and `mode` here. |
| `sepaDirectDebit` | Creates a SEPA Direct Debit instance when called with your options. It returns a `SepaDirectDebitInstance`. |
| `SepaDirectDebitInstance` | The instance returned by `sepaDirectDebit`. It exposes `.show()` to open the mandate collection pop-up and `.destroy()` to remove any rendered UI and clean up. |
| `createOrder` | A 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](/docs/api/merchant#create-order), receives the `token`, and returns `{ publicId: order.token }` to the widget, so it can start the checkout session. |
| Instance methods | Call `.show()` to open the SEPA Direct Debit pop-up. Call `.destroy()` to close the widget and clean up resources. |

:::info
For more details about the available parameters, see: [Merchant Web SDK: SEPA Direct Debit](/docs/sdks/merchant-web-sdk/payment-methods/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.

1. **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`](/docs/sdks/merchant-web-sdk/types/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:

- ![With async await]

  ```js [my-app.js] {19-23,25-28,30-33}
  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()
  })
  ```

- ![Without async await]

  ```js [my-app.js] {19-22,24-27,29-32}
  import RevolutCheckout from '@revolut/checkout'

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

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    sepaDirectDebitButton.addEventListener('click', () => {
      const instance = sepaDirectDebit({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ 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}`)
        },

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

      instance.show()
    })
  })
  ```

| Callback | Description |
| -------- | ----------- |
| `onSuccess` | Fires 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. |
| `onError` | Fires when an error occurs. Receives [`RevolutCheckoutError`](/docs/sdks/merchant-web-sdk/types/revolut-checkout-error). Use it to display error messaging, log diagnostics, or prompt the user to try again. |
| `onCancel` | Fires 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. |

:::warning [Callbacks are not guaranteed]
Widget callbacks are not guaranteed to fire due to network issues, browser closures, or ad-blockers. Always use [webhooks](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks) for critical backend operations like order fulfilment.
:::

#### Webhooks (mandatory)

:::warning
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](/docs/api/merchant#create-webhook) endpoint:

- ![Request]

  Send a `POST` request to the [Create a webhook](/docs/api/merchant#create-webhook) endpoint with your webhook URL and the events you want to subscribe to:

  ```http [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"
    ]
  }
  ```

  | Parameter | Description | Required |
  |-----------|-------------|----------|
  | `url` | Your webhook URL. Must be a valid HTTP or HTTPS URL capable of receiving `POST` requests. | Yes |
  | `events` | Array of event types to subscribe to. See [Webhook events](/docs/api/merchant#create-webhook) for the full list. | Yes |

- ![Response]

  The API returns the created webhook object, including the `signing_secret` you'll use to [verify webhook payloads](/docs/guides/merchant/monitor-and-observe/webhooks/verify-the-payload-signature):

  ```json [Response example]
  {
    "id": "181c49cb-bedc-4ae2-9e1c-61d51b99867f",
    "url": "https://example.com/webhooks",
    "events": [
      "ORDER_CANCELLED",
      "ORDER_PAYMENT_DECLINED",
      "ORDER_COMPLETED",
      "ORDER_PAYMENT_FAILED",
      "ORDER_FAILED",
      "ORDER_AUTHORISED",
      "ORDER_PAYMENT_AUTHORISATION_STARTED"
    ],
    "state": "ACTIVE",
    "signing_secret": "wsk_abcdEFGH123456..."
  }
  ```

  | Field | Description |
  |-------|-------------|
  | `id` | The unique identifier of the webhook subscription. |
  | `state` | The state of the webhook subscription. `ACTIVE` means events are being sent. |
  | `signing_secret` | Use this to verify the `Revolut-Signature` header on incoming webhook payloads. See [Verify the payload signature](/docs/guides/merchant/monitor-and-observe/webhooks/verify-the-payload-signature). |

:::info
For detailed information on setting up webhooks, see: [Use webhooks to track order and payment lifecycle](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks).
:::

##### Payment outcomes

<!-- TODO: ORDER_AUTHORISED and ORDER_FAILED are included in the webhook subscription but not yet documented in Payment outcomes. Clarify with PO/dev what these events signal for SEPA DD and what actions merchants should take. -->

Handle the following scenarios:

- ![Payment submitted]

  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 event | Description |
  |---------------|-------------|
  | `ORDER_PAYMENT_AUTHORISATION_STARTED` | The payment has been submitted to the SEPA scheme |

  **What to do:**

  1. Pull the mandate reference and IBAN details from the [Retrieve an order](/docs/api/merchant#retrieve-order) endpoint. Check for the following fields in `payments[].payment_method`:

      | Field | Description |
      |-------|-------------|
      | `mandate_reference` | The unique mandate reference number generated by Revolut |
      | `debtor_iban_last_four` | The last 4 digits of the customer's IBAN |
      | `debtor_name` | The account holder's name |

  1. Send the mandate confirmation and pre-notification to the customer as per SEPA rules.

  :::note
  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.
  :::

- ![Payment completed]

  The payment has been captured and completed.

  **Webhook expected:**

  | Webhook event | Description |
  |---------------|-------------|
  | `ORDER_COMPLETED` | The payment has been captured and completed |

  **What to do:**

  Fulfil the order (ship goods, grant access, etc.) once `ORDER_COMPLETED` is received.

  Since `isRecurringPayment` defaults to `true`, the payment method is saved for the merchant. After `ORDER_COMPLETED`, [retrieve the payment](/docs/api/merchant#retrieve-payment-details) to obtain `payment_method.id` - store it for subsequent retries and recurring charges via `POST /api/payments`.

- ![Payment failure]

  The payment was declined or failed.

  **Webhooks expected:**

  | Webhook event | Description |
  |---------------|-------------|
  | `ORDER_PAYMENT_DECLINED` | Payment was declined by the debtor's bank |
  | `ORDER_PAYMENT_FAILED` | Payment failed due to a technical error |
  | `ORDER_CANCELLED` | Order was cancelled |

  **What to do:**

  1. Retrieve the [order](/docs/api/merchant#retrieve-order) or [payment](/docs/api/merchant#retrieve-payment-details) details to get the `decline_reason`.
  1. Notify the customer and direct them to update their payment details or use a different payment method.

  :::info
  See the [list of decline reasons](/docs/guides/merchant/reference/error-codes/decline-reasons) to understand why the payment was unsuccessful.
  :::

  **Retry with saved payment method:**

  Since the payment method is saved by default (`isRecurringPayment` defaults to `true`), you can retry using the **saved payment method ID** after a failed payment. 
  
  The `payment_method.id` is available from the payment response after `ORDER_COMPLETED` of the initial payment or order, you can find it in the [order](/docs/api/merchant#retrieve-order) or [payment](/docs/api/merchant#retrieve-payment-details) details. Use it in subsequent payment requests as `saved_payment_method.id` with `initiator: "merchant"`.

  ```json [Retry payment request] {3-5}
  {
    "saved_payment_method": {
      "type": "sepa_direct_debit",
      "id": "2b83c23a-650e-40c3-8989-00ee24478738",
      "initiator": "merchant"
    }
  }
  ```

#### 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:**

| Requirement | Details |
|-------------|---------|
| **Mandatory fields** | Debtor name, IBAN, creditor identifier, mandate reference, customer consent |
| **Storage** | Store mandate records securely for as long as they remain valid and for **at least 14 months** after the last collection |
| **Mandate reference** | Revolut 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 readiness** | Be 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 content | Details |
|-----------------|---------|
| **Last 4 digits of IBAN** | The last 4 digits of the debtor's bank account number |
| **Mandate reference** | The unique reference number of the mandate |
| **Debit amount** | The amount to be debited |
| **Creditor identifier** | Your SEPA creditor identifier |
| **Contact information** | Your business contact details |

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

:::warning
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

- [:CodeRepository: Explore our integration examples repository](https://github.com/revolut-engineering/revolut-checkout-example "Discover all available examples and see how different payment solutions are implemented")

### Example with minimal required parameters

- ![With async await]

  ```js [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()
  })
  ```

- ![Without async await]

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

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

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    sepaDirectDebitButton.addEventListener('click', () => {
      const instance = sepaDirectDebit({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ 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

- ![With async await]

  ```js [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()
  })
  ```

- ![Without async await]

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

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

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    sepaDirectDebitButton.addEventListener('click', () => {
      const instance = sepaDirectDebit({
        createOrder: () => {
          // Call your backend here to create an order and return order.token
          return yourServerSideCall()
            .then((order) => ({ 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`:

- ![With async await]

  ```js [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()
  })
  ```

- ![Without async await]

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

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

  RevolutCheckout.payments({
    publicToken: 'pk_abcdef12347890_...', // Merchant public API key
    mode: 'prod',
  }).then(({ sepaDirectDebit }) => {
    sepaDirectDebitButton.addEventListener('click', () => {
      const instance = sepaDirectDebit({
        createOrder: () => {
          return yourServerSideCall()
            .then((order) => ({ 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

:::warning
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](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks) 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](/docs/api/merchant#retrieve-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](/docs/guides/merchant/accept-payments/online-payments/sepa-direct-debit/introduction#available-countries).
- [ ] 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](/docs/guides/merchant/test-and-go-live/testing/implementation-checklists).

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

## What's next

- [:CodeRepository: SEPA Direct Debit Web SDK reference](/docs/sdks/merchant-web-sdk/payment-methods/sepa-direct-debit 'Full SDK reference for the web SDK')
- [:ArrowExchange: Order and payment lifecycle](/docs/guides/merchant/reference/order-lifecycle 'Understand how orders and payments are processed')
- [:Webhook: Using webhooks](/docs/guides/merchant/monitor-and-observe/webhooks/using-webhooks 'Track the payment lifecycle with server-to-server webhooks')
- [:HelpChat: Decline reasons](/docs/guides/merchant/reference/error-codes/decline-reasons 'Understand why SEPA DD payments may be declined')
- [:Repayment: Refunds](/docs/guides/merchant/operations/refunds 'Learn how to refund your orders')
- [:DocumentChecked: Order management](/docs/api/merchant#retrieve-order-list 'Explore the full capabilities of the Orders API')