Dunning & Payment Recovery
RevKeen's smart retry engine automatically recovers failed payments and reduces involuntary churn
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:
1. Classify Decline
2. Smart Retry
3. Notify Customer
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 |
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:
"We couldn't charge your card. Please update your payment method to avoid service interruption."
"Still having trouble charging your card. We'll retry again soon, but please update your payment method."
"Action needed to avoid service interruption. Update your payment method now."
"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 |
auto_cancel_unpaid is set to true, the subscription will automatically cancel after all retries are exhausted. Otherwise, it remains in unpaid status 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');
}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!');
}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 |
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
customer.update_payment_methodwebhook to prompt updates before expiration.