Dunning
Dunning and revenue-recovery automation — methods, parameters, and examples for @easylabs/node.
Dunning controls what happens when a recurring charge fails: how often Easy Labs retries, what email goes out, what the recovery page looks like, and what the subscription / invoice transitions to once retries are exhausted. Revenue-recovery automations layer on top — event-triggered, condition-gated, multi-action workflows (e.g. "after invoice_overdue, if amount > $500, email Sales").
Methods
Dunning config
The dunning config is a singleton per company.
easy.createOrReplaceDunningConfig(body); // POST /dunning-config
easy.getDunningConfig(); // GET /dunning-config
easy.updateDunningConfig(body); // PATCH /dunning-configawait easy.createOrReplaceDunningConfig({
retry_mode: "smart",
smart_retry_attempts: 8,
smart_retry_window: "2_weeks",
bank_debit_retries_enabled: true,
bank_debit_retry_schedule: [3, 7, 14], // days after failure
subscription_terminal_action: "cancel", // "cancel" | "unpaid" | "past_due" | "pause"
invoice_terminal_action: "uncollectible", // "past_due" | "uncollectible"
payment_failed_email_enabled: true,
expiring_card_email_enabled: true,
card_expiry_warn_days: 30,
payment_failed_recovery_page_mode: "hosted",
expiring_card_recovery_page_mode: "custom_link",
expiring_card_custom_link_url: "https://app.example.com/billing/update-card",
});Switch to retry_mode: "custom" to drive the cadence yourself with custom_retry_schedule: number[] (days after failure).
Revenue-recovery automations
easy.listRevenueRecoveryAutomations(); // GET /revenue-recovery-automations
easy.createRevenueRecoveryAutomation(body); // POST /revenue-recovery-automations
easy.updateRevenueRecoveryAutomation(id, body); // PATCH /revenue-recovery-automations/:id
easy.deleteRevenueRecoveryAutomation(id); // DELETE /revenue-recovery-automations/:id
easy.listRevenueRecoveryAutomationRuns(id); // GET /revenue-recovery-automations/:id/runsawait easy.createRevenueRecoveryAutomation({
name: "Big-ticket overdue → Sales",
trigger_type: "invoice_overdue",
conditions: [
{ type: "invoice_amount", operator: "more_than", amount_cents: 50000 },
],
actions: [
{
type: "email_team_member",
delay_days: 0,
recipient_email: "sales@example.com",
note: "High-value overdue invoice — please follow up.",
},
],
active: true,
});Object shape
DunningConfig
A single document. Highlights:
| Field | Type | Notes |
|---|---|---|
retry_mode | "smart" | "custom" | |
smart_retry_attempts | 4 | 8 | When retry_mode = "smart". |
smart_retry_window | "1_week" | "2_weeks" | "3_weeks" | "1_month" | "2_months" | |
custom_retry_schedule | number[] | Days after failure, when retry_mode = "custom". |
bank_debit_retries_enabled / bank_debit_retry_schedule | boolean / number[] | Bank-debit-specific retry policy. |
subscription_terminal_action | "cancel" | "unpaid" | "past_due" | "pause" | What happens when retries are exhausted. |
invoice_terminal_action | "past_due" | "uncollectible" | |
payment_failed_email_enabled / expiring_card_email_enabled / email_action_required | boolean | |
card_expiry_warn_days | number | |
payment_failed_recovery_page_mode / expiring_card_recovery_page_mode | "hosted" | "custom_link" | |
payment_failed_custom_link_url / expiring_card_custom_link_url | string | null |
Revenue-recovery automation
CreateRevenueRecoveryAutomation:
| Field | Type |
|---|---|
trigger_type | "invoice_due_date_upcoming" | "invoice_finalized" | "invoice_overdue" | "subscription_payment_failed" | "subscription_canceled" |
conditions[] | { type: "invoice_amount" | "invoice_metadata" | "product_on_invoice"; operator?, amount_cents?, key?, value?, product_id?, product_name? } |
actions[] | { type: "email_team_member" | "mark_invoice_uncollectible"; delay_days?, recipient_user_id?, recipient_email?, recipient_name?, note? } |
active | boolean |
The list endpoint and run records are currently typed as Record<string, unknown>[] pending a stronger schema.
Examples
Switch to custom retry cadence
await easy.updateDunningConfig({
retry_mode: "custom",
custom_retry_schedule: [1, 3, 7, 14, 30],
});Disable a dunning automation without deleting it
await easy.updateRevenueRecoveryAutomation(automationId, { active: false });Audit recent runs
const runs = await easy.listRevenueRecoveryAutomationRuns(automationId);
console.table(runs.data);