Easy Labs
BillingGuides

Configure dunning

Set retry policy, recovery emails, and terminal actions for failed payments.

Goal

Define how the platform retries failed recurring charges, what the customer sees when a payment fails, and what state the subscription or invoice ends up in if recovery exhausts. There is exactly one dunning config per merchant — set it once, then iterate. Pair this with revenue-recovery automations for event-driven follow-up beyond retries.

Prerequisites

  • A sandbox or production API key
  • @easylabs/node installed
  • Active subscriptions and/or charge_automatically invoices — dunning has nothing to do until something fails

Implementation

1. Set the retry policy

Pick smart for a platform-tuned schedule sized by smart_retry_window and smart_retry_attempts, or custom if you need precise day offsets.

import { createClient } from "@easylabs/node";

const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });

await easy.createOrReplaceDunningConfig({
  retry_mode: "smart",
  smart_retry_attempts: 8,
  smart_retry_window: "2_weeks",

  // Bank-debit failures need their own track (longer settlement windows).
  bank_debit_retries_enabled: true,
  bank_debit_retry_schedule: [3, 7, 14],

  // Customer-facing emails on failure / expiring card.
  payment_failed_email_enabled: true,
  expiring_card_email_enabled: true,
  card_expiry_warn_days: 14,

  // Where the recovery email sends the customer.
  payment_failed_recovery_page_mode: "custom_link",
  payment_failed_custom_link_url: "https://your-app.com/billing/recover",

  // Terminal actions when retries are exhausted.
  subscription_terminal_action: "past_due",
  invoice_terminal_action: "uncollectible",
});

2. Read or patch later

createOrReplaceDunningConfig is destructive — it replaces every field. Use updateDunningConfig to change a subset:

await easy.updateDunningConfig({ smart_retry_attempts: 4 });
const { data: current } = await easy.getDunningConfig();

3. Add revenue-recovery automations (optional)

Automations are conditional rules layered on top of the retry engine. They fire on triggers like invoice_overdue and run actions like email_team_member or mark_invoice_uncollectible.

await easy.createRevenueRecoveryAutomation({
  name: "Notify CSM on enterprise overdue",
  trigger_type: "invoice_overdue",
  conditions: [
    { type: "invoice_amount", operator: "more_than", amount_cents: 100_000 },
  ],
  actions: [
    { type: "email_team_member", delay_days: 1, recipient_email: "csm@your-app.com" },
  ],
  active: true,
});

4. Listen for terminal events

Subscribe to invoice.payment_failed, invoice.marked_uncollectible, subscription.updated (status transitions to past_due / unpaid), and revenue_recovery.action_completed to log dunning outcomes and notify your team.

Tradeoffs

  • smart vs. custom retriessmart adapts retry timing to issuer behavior and typically recovers more revenue than a fixed schedule; reach for custom only when you have hard requirements (e.g. quiet hours).
  • Terminal action cancel vs. past_duecancel ends the subscription and stops downstream charges; past_due keeps it visible so a customer can self-recover via the portal. Most SaaS picks past_due + customer-portal recovery.
  • Recovery page mode hosted vs. custom_link — hosted is zero-effort and white-labeled; custom_link lets you deep-link to a customer-portal magic link with the failure context already loaded.

On this page