Dunning
Recover failed payments automatically with smart retry schedules and customer notifications
When a subscription payment fails, RevKeen automatically enters a dunning process that intelligently retries payments while keeping your customers informed. This process typically recovers 30-50% of failed payments without any manual intervention.
How Dunning Works
When a payment fails, RevKeen classifies the decline code and determines the best recovery strategy:
- Classify Decline -- Analyze the decline code to determine if it's a temporary (soft) or permanent (hard) decline.
- Smart Retry -- Schedule automatic retries based on decline type. Soft declines get retried on Days 1, 3, and 7.
- Notify Customer -- Send targeted emails at each stage to encourage customers to update their payment method.
Decline Code Categories
| Category | Description | Action | Example Codes |
|---|---|---|---|
| Hard Decline | Card permanently blocked or invalid | No retry -- request new card | 200, 204, 303, 530, 531 |
| Soft Decline | Temporary issue (insufficient funds, etc.) | Auto-retry on schedule | 300, 301, 304, 402, 521 |
| Action Required | Customer must update card | Email customer to update | 202, 223, 225 |
Default Retry Schedule
The default retry schedule balances recovery rates with customer experience:
| Day | Action | Email Sent | Service Access |
|---|---|---|---|
| Day 0 | Payment fails, subscription enters past_due | "Payment Failed" | Full access |
| Day 1 | First retry attempt | None (if successful) / "Retry Failed" | Full access |
| Day 3 | Second retry attempt | "Urgent: Update Payment Method" | Warning shown |
| Day 7 | Final retry attempt | "Service Suspended" (if failed) | Suspended |
You can customize the retry schedule and grace period in your client settings. Longer grace periods may recover more payments but also increase revenue delay.
Grace Period Configuration
The grace period determines how long customers retain access while their payment is being retried. Configure these settings per-client:
| Setting | Description | Default |
|---|---|---|
grace_period_days | Days before service suspension | 7 |
max_retry_attempts | Maximum payment retry attempts | 3 |
retry_schedule | Days between retries | [1, 3, 7] |
dunning_email_enabled | Send dunning emails | true |
auto_cancel_unpaid | Cancel after max retries exhausted | false |
Dunning Email Sequence
RevKeen sends a series of emails to help customers recover their subscription:
Day 0: Payment Failed -- "We couldn't charge your card. Please update your payment method to avoid service interruption."
Day 1: Retry Failed -- "Still having trouble charging your card. We'll retry again soon, but please update your payment method."
Day 3: Urgent Action Needed -- "Action needed to avoid service interruption. Update your payment method now."
Day 7: Service Suspended -- "Your subscription has been suspended due to payment failure. Update your payment method to restore access."
Subscription States During Dunning
| State | Description | Service Access |
|---|---|---|
past_due | Payment failed, in grace period with retries scheduled | Limited |
unpaid | All retries exhausted, service suspended | None |
canceled | Terminated after dunning (if auto_cancel_unpaid=true) | None |
When
auto_cancel_unpaidis set totrue, the subscription will automatically cancel after all retries are exhausted. Otherwise, it remains inunpaidstatus until manually resolved.
Handling Dunning via API
While dunning is automatic, you can interact with it programmatically.
Check Subscription Dunning Status
const subscription = await client.subscriptions.retrieve('sub_xxxxxxxx');
if (subscription.data.status === 'past_due') {
console.log('Subscription is in dunning');
console.log('Grace period ends:', subscription.data.current_period_end);
}
if (subscription.data.status === 'unpaid') {
console.log('All retries exhausted - needs manual intervention');
}subscription = client.subscriptions.retrieve("sub_xxxxxxxx")
if subscription.data.status == "past_due":
print("Subscription is in dunning")
print(f"Grace period ends: {subscription.data.current_period_end}")
if subscription.data.status == "unpaid":
print("All retries exhausted - needs manual intervention")Manually Retry a Failed Payment
// Get the past-due invoice
const invoices = await client.invoices.list({
subscription: 'sub_xxxxxxxx',
status: 'past_due',
});
// Manually retry payment
const result = await client.invoices.pay(invoices.data[0].id);
if (result.data.status === 'paid') {
console.log('Payment recovered successfully!');
}# Get the past-due invoice
invoices = client.invoices.list(
subscription="sub_xxxxxxxx",
status="past_due"
)
# Manually retry payment
result = client.invoices.pay(invoices.data[0].id)
if result.data.status == "paid":
print("Payment recovered successfully!")Dunning Webhooks
Listen to these webhooks to stay informed about dunning progress:
| Event | Description |
|---|---|
invoice.payment_failed | Payment attempt failed |
invoice.payment_action_required | Customer action needed (e.g., 3DS verification) |
subscription.past_due | Subscription entered past_due status |
subscription.unpaid | All retries exhausted |
invoice.paid | Payment recovered |
invoice.marked_uncollectible | Invoice written off as bad debt |
Webhook Handler Example
app.post('/webhooks/revkeen', async (req, res) => {
const event = req.body;
switch (event.type) {
case 'invoice.payment_failed':
// Log failed payment, maybe trigger internal notification
console.log('Payment failed for invoice:', event.data.id);
break;
case 'subscription.past_due':
// Optionally show in-app warning to customer
await showPastDueBanner(event.data.customer_id);
break;
case 'subscription.unpaid':
// Suspend service access
await suspendAccess(event.data.customer_id);
break;
case 'invoice.paid':
// Payment recovered! Restore access
await restoreAccess(event.data.customer_id);
break;
}
res.status(200).send('OK');
});Best Practices
- Keep Cards Updated -- Encourage customers to enable card updates with their bank. Use the
customer.update_payment_methodwebhook to prompt updates before expiration. - Grace Period Length -- 7 days is the sweet spot for most businesses -- long enough to recover payments, short enough to minimize revenue lag.
- Custom Retry Schedule -- For high-value subscriptions, consider extending retries to 14 days with more attempts. For low-value, shorter schedules reduce bad debt.
- Monitor Recovery Rates -- Track your dunning recovery rate in analytics. Aim for 30-50% recovery of failed payments. Below 20% may indicate card quality issues.
Warning: Never disable dunning emails completely. They are proven to significantly improve recovery rates and reduce involuntary churn.
Related
- Subscriptions -- Learn about the full subscription lifecycle
- Payments and Refunds -- Processing payments and issuing refunds