SEPA Direct Debit: Recurring Collections using Mandates and Workflows

Learn how to let customers fund their account by SEPA Direct Debit: create and confirm a mandate, then set up a recurring collection with a workflow.

With SEPA Direct Debit, your customers fund their account by pull payment instead of a bank transfer. The customer authorizes lemon.markets once to collect from their own bank account, and you set up a recurring collection of a fixed amount into their cash account, e.g. 150.00 EUR on the 15th of every month. Collections fund the account automatically, so customers invest regularly without having to remember a transfer.

This takes two resources:

  • A payment instrument mandate (pinst_mand_…) is the customer's authorization to collect from one bank account. You create it, and the customer confirms it, with strong customer authentication (SCA) if it is enabled for you.
  • A workflow (wf_…) with the action deposit.collect_with_mandate is the recurring collection: which mandate, which cash account, how much and how often. It is the same workflow resource you already use for savings plans.

One confirmed mandate can back several workflows.

NOTE: The payment instrument mandate endpoints and direct-debit collections are in preview.

Before You Start

  • Collections are enabled per partner. To enable them, contact your partner manager. Until then, creating a collection workflow is rejected with a 422.
  • The customer account needs a cash account to receive the collections: the cash_account of its securities account (see List Securities Accounts).
  • Subscribe to the payment_instrument_mandate.*, workflow.* and deposit.* events (see Events).
  • If strong customer authentication (SCA) is enabled for you, the customer confirms the mandate and the workflow on their device. See Strong Customer Authentication.

Overview

Setting up a recurring collection takes four steps:

  1. Create a mandate for the customer's bank account.
  2. Present the mandate to the customer, who accepts it.
  3. Confirm the mandate.
  4. Create and confirm the collection workflow.

Steps 1 to 3 happen once per bank account. Step 4 happens once per collection plan.

Step 1: Create a Mandate

Create the mandate with the customer's bank account (the debtor account). The account holder name must use the SEPA Latin character set. Currently, it must also be the customer's own name, as on the customer account; any other name is rejected with the same 422 as an unusable bank account. Pre-fill the field with the customer's name.

curl -sS -H 'LMG-Data-Privacy-Access-Principal: backend-cusa_12345' \
    -H 'LMG-Data-Privacy-Access-Justification: app.create_mandate' \
    -H 'Authorization: Bearer <your-api-key>' \
    -H 'Content-Type: application/json; charset: utf-8' \
    -H 'Idempotency-Key: 3f6c1d2e-8a4b-4c7d-9e0f-1a2b3c4d5e6f' \
    'https://sandbox.api.lemon.markets/v1/accounts/cusa_12345/payment_instrument_mandates' \
    --data-binary '
    {
      "type": "sepa_direct_debit",
      "debtor": {
        "iban": "DE02120300000000202051",
        "bic": "BYLADEM1001",
        "account_holder_name": "Erika Mustermann"
      }
    }'

The Idempotency-Key header is optional and lets you retry the request safely. See Idempotency.

The response contains the created mandate. lemon.markets assigns the mandate_reference and the creditor data:

{
  "id": "pinst_mand_f3a42ba00cb84618a549d975662c90b3",
  "type": "sepa_direct_debit",
  "mandate_reference": "LM0A1B2C3D4E5F6G7H8J9K",
  "debtor": {
    "iban": "DE02120300000000202051",
    "bic": "BYLADEM1001",
    "account_holder_name": "Erika Mustermann"
  },
  "creditor": {
    "identifier": "DE98ZZZ09999999999",
    "name": "lemon.markets brokerage GmbH",
    "address": {
      "line_1": "Prinzessinnenstrasse 19-20",
      "postal_code": "10969",
      "city": "Berlin",
      "country": "DE"
    }
  },
  "sca": {
    "required": true,
    "challenge": "K7_yR-7ZpTqgeWQ4-bx7Kg=="
  },
  "history": [
    {
      "status": "created",
      "timestamp": "2026-10-19T09:15:00.123456+00:00"
    }
  ]
}

A created mandate cannot be used for collections yet. A customer account can have at most one confirmed mandate per bank account. If one already exists, reuse it or cancel it first. To find it, list the mandates with status=confirmed (see List Payment Instrument Mandates) and match debtor.iban. When the bank account cannot be used for a mandate, the request is rejected with a 422: ask the customer for a different bank account.

Step 2: Present the Mandate to the Customer

Before the customer confirms, show them the mandate in your app. This is a compliance step, not an API call, and it is your responsibility as the partner. Display:

  • the mandate wording lemon.markets provides to you;
  • the creditor data: creditor.identifier, creditor.name and creditor.address;
  • the debtor data: debtor.iban and debtor.account_holder_name;
  • the mandate_reference, which the customer will also see on their bank statement.

lemon.markets reviews your mandate presentation as part of validating your integration before it is released to customers.

When sca.required is true, the customer accepts the mandate by signing sca.challenge on their device (see Strong Customer Authentication). This is the same mechanism you use to confirm orders and workflows.

Step 3: Confirm the Mandate

Send the customer's signature to confirm the mandate:

curl -sS -H 'LMG-Data-Privacy-Access-Principal: backend-cusa_12345' \
    -H 'LMG-Data-Privacy-Access-Justification: app.confirm_mandate' \
    -H 'Authorization: Bearer <your-api-key>' \
    -H 'Content-Type: application/json; charset: utf-8' \
    'https://sandbox.api.lemon.markets/v1/accounts/cusa_12345/payment_instrument_mandates/pinst_mand_f3a42ba00cb84618a549d975662c90b3/confirm' \
    --data-binary '
    {
      "sca": {
        "client_credential_id": "cred_950eecf22d3b493b9fb47de80775ee16",
        "signature": "MEUCIQ..."
      }
    }'

When sca.required is false, send an empty object ({}) instead.

The mandate changes to confirmed, and sca is no longer returned. The timestamp of the confirmed status is the mandate signing date.

{
  "id": "pinst_mand_f3a42ba00cb84618a549d975662c90b3",
  "type": "sepa_direct_debit",
  "mandate_reference": "LM0A1B2C3D4E5F6G7H8J9K",
  "debtor": {
    "iban": "DE02120300000000202051",
    "bic": "BYLADEM1001",
    "account_holder_name": "Erika Mustermann"
  },
  "creditor": {
    "identifier": "DE98ZZZ09999999999",
    "name": "lemon.markets brokerage GmbH",
    "address": {
      "line_1": "Prinzessinnenstrasse 19-20",
      "postal_code": "10969",
      "city": "Berlin",
      "country": "DE"
    }
  },
  "history": [
    {
      "status": "confirmed",
      "timestamp": "2026-10-19T09:16:00.654321+00:00"
    },
    {
      "status": "created",
      "timestamp": "2026-10-19T09:15:00.123456+00:00"
    }
  ]
}

Confirmation is rejected with a 422 when the signature is missing or doesn't verify the challenge, or when another mandate for the same bank account was confirmed in the meantime. A missing or invalid signature leaves the mandate created with the same challenge, so the customer can sign again, e.g. after an aborted prompt on their device; there's no need to create a new mandate. See Confirm Payment Instrument Mandate for every error.

Step 4: Set Up the Recurring Collection

Create the Workflow

Create a workflow with the schedule trigger and the deposit.collect_with_mandate action:

curl -sS -H 'LMG-Data-Privacy-Access-Principal: backend-cusa_12345' \
    -H 'LMG-Data-Privacy-Access-Justification: app.create_workflow' \
    -H 'Authorization: Bearer <your-api-key>' \
    -H 'Content-Type: application/json; charset: utf-8' \
    'https://sandbox.api.lemon.markets/v1/accounts/cusa_12345/workflows' \
    --data-binary '
    {
      "trigger": {
        "type": "schedule",
        "cadence": "monthly",
        "day_of_month": 15
      },
      "action": {
        "type": "deposit.collect_with_mandate",
        "deposit": {
          "payment_instrument_mandate": "pinst_mand_f3a42ba00cb84618a549d975662c90b3",
          "creditor_cash_account": "casha_5d0b9e2c7a1f4e3b8c6d2a9f0e1b7c43",
          "amount": "150.00",
          "currency": "EUR"
        }
      }
    }'

The deposit object takes:

  • payment_instrument_mandate: the confirmed mandate from step 3.
  • creditor_cash_account: the cash account that receives the collected amount. It must be one of the customer account's own cash accounts, e.g. the cash_account of its securities account (see List Securities Accounts).
  • amount: the fixed amount to collect each time, greater than 0 with at most two decimal places.
  • currency: EUR.

The schedule works as for savings plans: a weekly cadence with an optional day_of_week, or a monthly cadence with an optional day_of_month, and an optional start_after. On the sandbox, a daily cadence is also available for testing.

Currently, amount can be at most 1000.00 EUR per collection, and the amounts of all of the customer account's collection workflows in status created or confirmed can add up to at most 2500.00 EUR, whatever their cadence.

The response contains the created workflow. No appropriateness consent is required for a collection:

{
  "id": "wf_7c1e4b2a9d3f4e8ab6c0d5e2f1a3b9c8",
  "trigger": {
    "type": "schedule",
    "cadence": "monthly",
    "day_of_month": 15
  },
  "action": {
    "type": "deposit.collect_with_mandate",
    "deposit": {
      "payment_instrument_mandate": "pinst_mand_f3a42ba00cb84618a549d975662c90b3",
      "creditor_cash_account": "casha_5d0b9e2c7a1f4e3b8c6d2a9f0e1b7c43",
      "amount": "150.00",
      "currency": "EUR"
    }
  },
  "sca": {
    "required": true,
    "challenge": "Qm3x_V8pLrTz2wYk9HcN4A=="
  },
  "appropriateness_consent": {
    "required": false
  },
  "history": [
    {
      "status": "created",
      "timestamp": "2026-10-28T09:20:31.402518+00:00"
    }
  ]
}

Creation is rejected with a 404 when the mandate or the cash account doesn't belong to the customer account, and with a 422 when the mandate isn't confirmed, when a limit is exceeded, when the customer account isn't opened or belongs to a business customer, or when collections aren't enabled for you. See Create Workflow for every error.

If a create request times out, check the customer account's workflows with List Workflows before you retry, so you don't set up the same collection twice.

Confirm the Workflow

The customer confirms the collection plan like any other workflow. When sca.required is true, they sign the workflow's sca.challenge:

curl -sS -H 'LMG-Data-Privacy-Access-Principal: backend-cusa_12345' \
    -H 'LMG-Data-Privacy-Access-Justification: app.confirm_workflow' \
    -H 'Authorization: Bearer <your-api-key>' \
    -H 'Content-Type: application/json; charset: utf-8' \
    'https://sandbox.api.lemon.markets/v1/accounts/cusa_12345/workflows/wf_7c1e4b2a9d3f4e8ab6c0d5e2f1a3b9c8/confirm' \
    --data-binary '
    {
      "sca": {
        "client_credential_id": "cred_950eecf22d3b493b9fb47de80775ee16",
        "signature": "MEUCIQ..."
      }
    }'

When sca.required is false, send an empty object ({}) instead.

The workflow changes to confirmed:

{
  "id": "wf_7c1e4b2a9d3f4e8ab6c0d5e2f1a3b9c8",
  "trigger": {
    "type": "schedule",
    "cadence": "monthly",
    "day_of_month": 15,
    "next_run_date": "2026-11-16"
  },
  "action": {
    "type": "deposit.collect_with_mandate",
    "deposit": {
      "payment_instrument_mandate": "pinst_mand_f3a42ba00cb84618a549d975662c90b3",
      "creditor_cash_account": "casha_5d0b9e2c7a1f4e3b8c6d2a9f0e1b7c43",
      "amount": "150.00",
      "currency": "EUR"
    }
  },
  "history": [
    {
      "status": "confirmed",
      "timestamp": "2026-10-28T09:21:02.117384+00:00"
    },
    {
      "status": "created",
      "timestamp": "2026-10-28T09:20:31.402518+00:00"
    }
  ]
}

For a collection, next_run_date is the collection date: the day the amount is collected from the customer's bank account. It is always a TARGET2 business day; when the scheduled day isn't one, for example 1 January or 1 May, the collection moves to the next business day. The first collection is a few business days after the workflow is confirmed at the earliest, so it can be later than the next occurrence of the scheduled day. Show the customer this date rather than one you calculate yourself. After each collection, next_run_date moves to the next collection date.

Collection Runs

Each collection is a deposit into the workflow's creditor_cash_account, with payment_instrument_mandate set to the mandate it collects with:

  1. A few days before the collection date, lemon.markets creates the deposit and sends deposit.created. When you receive it, send the customer a pre-notification of the upcoming collection and its amount. This is your responsibility as the partner, like the mandate presentation.
  2. On the collection date, the amount is collected from the customer's bank account.
  3. Once the amount is credited to the cash account, the deposit is accepted and deposit.received is sent. Collected money becomes available cash only then, some time after the collection date. Keep this in mind when the customer also has a savings plan.

NOTE: For an ordinary deposit, deposit.created is sent after the money has reached lemon.markets. For a collection, it only announces the collection, and no money has arrived yet. If you treat deposit.created as received money, check payment_instrument_mandate first and wait for deposit.received.

To list the collections of a mandate, use List Deposits with the payment_instrument_mandate filter.

Cancel and Change

  • Stop a collection plan: cancel the workflow with Cancel Workflow. If its next collection was already announced, that deposit is canceled too and deposit.canceled is sent, unless the collection was already submitted to the bank: then it still goes ahead. The mandate stays usable for the customer's other workflows.
  • The customer revokes the mandate: cancel it with Cancel Payment Instrument Mandate. Its collection workflows are canceled automatically. canceled is final: to collect from the same bank account again, create and confirm a new mandate.
  • Change the amount or schedule: workflows are immutable. Cancel the workflow and create a new one with the same mandate; the customer doesn't need to sign the mandate again.
  • Change the bank account: cancel the mandate, which cancels its workflows, then create and confirm a new mandate and new workflows.

Events

The following events are delivered to your webhook:

  • payment_instrument_mandate.created, payment_instrument_mandate.confirmed and payment_instrument_mandate.canceled when the mandate enters that status.
  • payment_instrument_mandate.rejected when lemon.markets decides that the mandate cannot be confirmed or can no longer be used. The mandate's collection workflows are canceled automatically. Ask the customer to set up a new mandate.
  • workflow.created, workflow.confirmed and workflow.canceled for the collection workflow, as for any other workflow. workflow.canceled is also sent when the workflow is canceled with its mandate.
  • deposit.created, deposit.received and deposit.canceled for each collection (see Collection Runs).

Sequence Diagram

sequenceDiagram
    participant User
    participant App
    participant Backend
    participant 🍋.markets

    User->>App: Set Up Recurring Funding:<br/>Bank Account, Amount, Schedule
    App->>Backend: Create Mandate
    Backend->>🍋.markets: POST /payment_instrument_mandates
    🍋.markets->>Backend: 201 Created<br/>{id: pinst_mand_1234,<br/>sca.challenge if SCA is enabled}
    Backend->>App: Mandate Created
    App->>User: Present Mandate:<br/>Wording, Creditor, Debtor,<br/>Mandate Reference
    User->>App: Accept Mandate
    App->>Backend: Confirm Mandate
    Backend->>🍋.markets: POST<br/>/payment_instrument_mandates/pinst_mand_1234/confirm
    🍋.markets->>Backend: 200 Success
    Backend->>🍋.markets: POST /workflows
    🍋.markets->>Backend: 201 Created<br/>{id: wf_1234,<br/>sca.challenge if SCA is enabled}
    Backend->>App: Collection Plan Created
    App->>User: Prompt Confirmation:<br/>Amount, Schedule
    User->>App: Confirm Plan
    App->>Backend: Confirm Plan
    Backend->>🍋.markets: POST<br/>/workflows/wf_1234/confirm
    🍋.markets->>Backend: 200 Success
    Backend->>App: Success
    App->>User: Present Confirmed Plan

Going Live

Before you release collections to your customers, lemon.markets validates your integration, including the mandate presentation. Your partner manager then enables collections for you in production.

Current Restrictions

  • Collections run on a monthly or weekly schedule. The sandbox also provides a daily schedule for testing. On the sandbox, collections are announced with deposit.created the evening before the collection date and credited the next morning, so you can test the full flow in a day.
  • Collections are in EUR, for a fixed amount, within the limits above.
  • A customer account can have one confirmed mandate per bank account.
  • Workflows are immutable. To change a collection plan, cancel it and create a new one.

If your use-case exceeds these capabilities, contact your partner manager.


Did this page help you?