PlexySDK DOCS

Auto Rescue for SEPA

Intelligent retry strategies for SEPA Direct Debit payment failures and R-transactions

Auto Rescue for SEPA

Auto Rescue for SEPA automatically handles failed SEPA Direct Debit payments, including R-transactions (returns, refusals, and reversals). SEPA Direct Debit has unique timing requirements and failure modes that require specialized retry strategies.

SEPA Direct Debit Lifecycle

Understanding the SEPA payment lifecycle is essential for effective recovery:

R-Transaction Types

SEPA returns (R-transactions) have specific codes and meanings:

Return Codes (Most Common)

CodeNameDescriptionRetryable
AC01Incorrect Account NumberIBAN is wrongNo
AC04Closed AccountAccount has been closedNo
AC06Blocked AccountAccount is blockedMaybe
AG01Transaction ForbiddenNot authorized for DDNo
AG02Invalid Bank Operation CodeTechnical errorYes
AM04Insufficient FundsNot enough balanceYes
AM05Duplicate CollectionAlready debitedNo
BE05Unknown Creditor IDCreditor not recognizedNo
FF01Invalid File FormatTechnical errorYes
MD01No MandateMandate not found/cancelledNo
MD06Refund RequestCustomer requested refundNo
MS02Reason Not SpecifiedCustomer refusedMaybe
MS03Reason Not SpecifiedAgent refusedMaybe
RC01Bank Identifier IncorrectBIC is wrongNo
SL01Service Not AllowedDD not permittedNo

R-transactions typically arrive 2-5 business days after the debit date. Plexy automatically monitors for returns and initiates Auto Rescue when appropriate.

How Auto Rescue Handles SEPA

Enabling Auto Rescue for SEPA

curl -X POST https://api.plexypay.com/v2/payments \
  -H "x-api-key: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 4999,
    "currency": "EUR",
    "customer": "cus_abc123",
    "payment_method": "pm_sepa_debit_xyz789",
    "payment_method_options": {
      "sepa_debit": {
        "mandate": "md_abc123"
      }
    },
    "auto_rescue": {
      "enabled": true,
      "max_retries": 3,
      "retry_window_days": 21
    },
    "metadata": {
      "subscription_id": "sub_monthly_123"
    }
  }'
import { Plexy } from '@plexy/plexy-web';

const plexy = new Plexy({
  apiKey: process.env.PLEXY_SECRET_KEY,
});

const payment = await plexy.payments.create({
  amount: 4999,
  currency: 'EUR',
  customer: 'cus_abc123',
  payment_method: 'pm_sepa_debit_xyz789',
  payment_method_options: {
    sepa_debit: {
      mandate: 'md_abc123',
    },
  },
  auto_rescue: {
    enabled: true,
    max_retries: 3,
    retry_window_days: 21,
  },
  metadata: {
    subscription_id: 'sub_monthly_123',
  },
});

console.log('Payment status:', payment.status);
import os
from plexy import Plexy

plexy = Plexy(api_key=os.environ['PLEXY_SECRET_KEY'])

payment = plexy.payments.create(
    amount=4999,
    currency='EUR',
    customer='cus_abc123',
    payment_method='pm_sepa_debit_xyz789',
    payment_method_options={
        'sepa_debit': {
            'mandate': 'md_abc123',
        },
    },
    auto_rescue={
        'enabled': True,
        'max_retries': 3,
        'retry_window_days': 21,
    },
    metadata={
        'subscription_id': 'sub_monthly_123',
    },
)

print(f"Payment status: {payment.status}")

SEPA-Specific Configuration

Retry Timing

SEPA has minimum timing requirements:

ConstraintRequirement
Minimum retry delay2 business days after R-transaction
Due date lead timeD-2 for Core, D-1 for B2B
Pre-notification14 days before first debit, unless agreed otherwise
Settlement time2-5 business days

Configure timing accordingly:

const payment = await plexy.payments.create({
  amount: 4999,
  currency: 'EUR',
  customer: 'cus_abc123',
  payment_method: 'pm_sepa_debit_xyz789',
  auto_rescue: {
    enabled: true,
    sepa_options: {
      // Minimum days between retry attempts (SEPA rules require >= 2)
      min_retry_interval_days: 5,
      // Whether to send pre-notification for retries
      send_prenotification: true,
      // Days of pre-notification before debit (default 14)
      prenotification_days: 5, // Reduced if customer agreed
    },
    max_retries: 3,
  },
});

R-Code Strategies

Configure different strategies based on R-transaction codes:

const payment = await plexy.payments.create({
  amount: 4999,
  currency: 'EUR',
  customer: 'cus_abc123',
  payment_method: 'pm_sepa_debit_xyz789',
  auto_rescue: {
    enabled: true,
    r_code_strategies: {
      // Insufficient funds - retry around typical paydays
      AM04: {
        retry: true,
        schedule_days: [5, 12, 20], // Spread across month
        notify_customer: true,
      },
      // Blocked account - retry once, then investigate
      AC06: {
        retry: true,
        max_retries: 1,
        schedule_days: [7],
        escalate_on_failure: true,
      },
      // Mandate cancelled - do not retry
      MD01: {
        retry: false,
        action: 'contact_customer',
        deactivate_mandate: true,
      },
      // Customer refund request - do not retry
      MD06: {
        retry: false,
        action: 'process_refund',
      },
    },
  },
});

Webhook Events

payment.sepa_return_received

Sent when an R-transaction is received.

{
  "id": "evt_abc123",
  "type": "payment.sepa_return_received",
  "created_at": "2026-03-28T09:15:00Z",
  "data": {
    "payment": "pay_xyz789",
    "customer": "cus_abc123",
    "amount": 4999,
    "currency": "EUR",
    "mandate": "md_abc123",
    "r_transaction": {
      "code": "AM04",
      "reason": "Insufficient Funds",
      "received_at": "2026-03-28T09:15:00Z",
      "original_debit_date": "2026-03-25"
    },
    "auto_rescue": {
      "enabled": true,
      "will_retry": true,
      "next_retry_at": "2026-04-02T10:00:00Z"
    }
  }
}

payment.rescue_retry (SEPA)

Sent when a SEPA retry is being attempted.

{
  "id": "evt_def456",
  "type": "payment.rescue_retry",
  "created_at": "2026-04-02T10:00:00Z",
  "data": {
    "payment": "pay_xyz789",
    "customer": "cus_abc123",
    "payment_method_type": "sepa_debit",
    "retry_number": 1,
    "previous_r_code": "AM04",
    "debit_date": "2026-04-07",
    "prenotification_sent": true,
    "retry_strategy": {
      "r_code_category": "insufficient_funds",
      "timing_reason": "payday_alignment"
    }
  }
}

payment.rescue_succeeded (SEPA)

Sent when a SEPA payment is successfully recovered.

{
  "id": "evt_ghi789",
  "type": "payment.rescue_succeeded",
  "created_at": "2026-04-10T14:30:00Z",
  "data": {
    "payment": "pay_xyz789",
    "customer": "cus_abc123",
    "amount": 4999,
    "currency": "EUR",
    "retry_number": 1,
    "total_attempts": 2,
    "days_to_recover": 15,
    "settlement_date": "2026-04-10"
  }
}

sepa_mandate.requires_attention

Sent when a mandate issue is detected.

{
  "id": "evt_jkl012",
  "type": "sepa_mandate.requires_attention",
  "created_at": "2026-03-28T09:20:00Z",
  "data": {
    "mandate": "md_abc123",
    "customer": "cus_abc123",
    "issue": "mandate_cancelled",
    "r_code": "MD01",
    "triggered_by_payment": "pay_xyz789",
    "recommendation": "Contact customer to set up a new mandate"
  }
}

Handling R-Transaction Types

Insufficient Funds (AM04)

The most common return reason, often resolved with proper timing.

// Configure payday-optimized retries for AM04
const payment = await plexy.payments.create({
  amount: 4999,
  currency: 'EUR',
  customer: 'cus_abc123',
  payment_method: 'pm_sepa_debit_xyz789',
  auto_rescue: {
    enabled: true,
    r_code_strategies: {
      AM04: {
        retry: true,
        // Retry on dates aligned with common paydays in EU
        // 1st: Month start, 15th: Mid-month, 25th: End of month
        schedule_days: [5, 16, 26],
        max_retries: 3,
        notify_customer_after_attempt: 1,
      },
    },
  },
});

Account Closed (AC04) or Incorrect (AC01)

Permanent issues requiring new account details.

app.post('/webhooks/plexy', async (req, res) => {
  const event = verifyWebhook(req);

  if (event.type === 'payment.sepa_return_received') {
    const { r_transaction, customer } = event.data;

    if (['AC01', 'AC04'].includes(r_transaction.code)) {
      // Deactivate the payment method
      await plexy.paymentMethods.update(event.data.payment_method, {
        active: false,
        deactivation_reason: r_transaction.reason,
      });

      // Contact customer for new account details
      await sendEmail(customer, 'sepa_account_invalid', {
        reason: r_transaction.reason,
        update_url: 'https://yoursite.com/update-payment',
      });
    }
  }

  res.status(200).send('OK');
});

Mandate Issues (MD01, MD06)

Mandate-related returns require special handling.

app.post('/webhooks/plexy', async (req, res) => {
  const event = verifyWebhook(req);

  if (event.type === 'payment.sepa_return_received') {
    const { r_transaction, mandate, customer } = event.data;

    if (r_transaction.code === 'MD01') {
      // Mandate not found or cancelled
      await plexy.mandates.update(mandate, {
        status: 'cancelled',
        cancellation_reason: 'r_transaction_md01',
      });

      await sendEmail(customer, 'mandate_cancelled', {
        setup_url: 'https://yoursite.com/setup-mandate',
      });
    }

    if (r_transaction.code === 'MD06') {
      // Customer requested refund - process it
      await plexy.refunds.create({
        payment: event.data.payment,
        reason: 'customer_request_via_bank',
      });

      // Note: Consider if subscription should be cancelled
      await flagForReview(customer, 'sepa_refund_requested');
    }
  }

  res.status(200).send('OK');
});

SEPA Core vs B2B

Auto Rescue handles both SEPA schemes:

AspectSEPA CoreSEPA B2B
Refund right8 weeks (no questions)None
Mandate verificationOptionalRequired
Minimum lead timeD-2D-1
Retry approachMore conservativeCan be more aggressive

Configure by Scheme

const payment = await plexy.payments.create({
  amount: 4999,
  currency: 'EUR',
  customer: 'cus_abc123',
  payment_method: 'pm_sepa_debit_xyz789',
  payment_method_options: {
    sepa_debit: {
      mandate: 'md_abc123',
      scheme: 'b2b', // or 'core'
    },
  },
  auto_rescue: {
    enabled: true,
    sepa_options: {
      scheme_specific: {
        core: {
          max_retries: 2, // Conservative for Core (refund risk)
          retry_window_days: 14,
        },
        b2b: {
          max_retries: 3, // More aggressive for B2B
          retry_window_days: 21,
        },
      },
    },
  },
});

Pre-Notification Management

SEPA requires customer notification before debits:

// Configure pre-notification for retries
const payment = await plexy.payments.create({
  amount: 4999,
  currency: 'EUR',
  customer: 'cus_abc123',
  payment_method: 'pm_sepa_debit_xyz789',
  auto_rescue: {
    enabled: true,
    sepa_options: {
      send_prenotification: true,
      prenotification_days: 5, // 5 days before debit
      prenotification_template: 'retry_notification',
    },
  },
});

Pre-Notification Webhook

{
  "id": "evt_mno345",
  "type": "sepa_prenotification.sent",
  "created_at": "2026-03-27T10:00:00Z",
  "data": {
    "payment": "pay_xyz789",
    "customer": "cus_abc123",
    "amount": 4999,
    "currency": "EUR",
    "debit_date": "2026-04-01",
    "is_retry": true,
    "retry_number": 1,
    "notification_type": "email"
  }
}

Best Practices

Timing Around Paydays

In Europe, common paydays include:

  • End of month (25th-31st)
  • Mid-month (15th)
  • Beginning of month (1st-5th)
// Optimal retry schedule for EU paydays
auto_rescue: {
  enabled: true,
  sepa_options: {
    // Schedule debits for 2-3 days after typical paydays
    preferred_debit_days: [3, 17, 28],
  },
}

Monitor Mandate Health

Track mandate-related returns:

// Fetch mandate analytics
const mandateHealth = await plexy.analytics.getMandateHealth({
  start_date: '2026-01-01',
  end_date: '2026-03-31',
});

// Response includes:
// {
//   "total_mandates": 5234,
//   "active": 5012,
//   "cancelled_by_customer": 89,
//   "cancelled_by_r_transaction": 133,
//   "r_transaction_breakdown": {
//     "MD01": 98,
//     "MD06": 35
//   }
// }

Respect Customer Requests

When you receive MD06 (refund request by customer), consider:

  1. Processing the refund promptly
  2. Reviewing the customer's subscription status
  3. Reaching out to understand the reason
  4. Potentially pausing future debits
if (r_code === 'MD06') {
  // Customer went to their bank for a refund - take it seriously
  await pauseSubscription(customer);
  await sendEmail(customer, 'subscription_paused', {
    reason: 'bank_refund_request',
    contact_us: true,
  });
}

Know When to Stop

Unlike cards, SEPA retries should be more conservative:

FactorRecommendation
Max retries2-3 (not 4-5 like cards)
Window14-21 days
After MD01/MD06Never retry
Multiple AM04sConsider contacting customer

Next Steps

On this page