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/nodeinstalled- Active subscriptions and/or
charge_automaticallyinvoices — 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
smartvs.customretries —smartadapts retry timing to issuer behavior and typically recovers more revenue than a fixed schedule; reach forcustomonly when you have hard requirements (e.g. quiet hours).- Terminal action
cancelvs.past_due—cancelends the subscription and stops downstream charges;past_duekeeps it visible so a customer can self-recover via the portal. Most SaaS pickspast_due+ customer-portal recovery. - Recovery page mode
hostedvs.custom_link— hosted is zero-effort and white-labeled;custom_linklets you deep-link to a customer-portal magic link with the failure context already loaded.