# Dunning Management

How AuxCORE handles failed recurring payments: automatic retries, customer notices, and recovery tools.

---

## Overview

When a scheduled charge is declined, AuxCORE automatically:

- 🔄 **Auto-retries** - Retries the charge on a fixed schedule
- 📧 **Notifies the customer** - Payment-failed email after each failed run, a dunning notice when retries run out
- ⏸️ **Auto-pauses** - Subscriptions are paused after the last retry; payment plans become `defaulted`
- 📊 **Tracks attempts** - Failure counters and an activity log on every schedule

All endpoints on this page use admin authentication (`Authorization: Bearer YOUR_JWT` + `X-Tenant-ID`). See [Subscriptions](./subscriptions.md).

---

## How Dunning Works

### Subscriptions (recurring schedules)

```
Payment fails → retry after 24h → retry after 24h → ... → max attempts reached → Pause + dunning email
```

- Each schedule allows `maxRetryAttempts` retries (default **3**).
- After each decline, `failCount` and `failedPaymentCount` increase, `lastFailedPaymentDate` is set and the next retry is scheduled **24 hours** later (`nextRetryAt`).
- Due charges are processed every hour; failed charges are also swept every 6 hours.
- When the retries are exhausted the schedule becomes `paused` (`isRecurring: false`, `pausedReason`: "Payment failed after 3 retry attempts") and a dunning email is sent once (only when the schedule has `sendNotifications: true` and the merchant's `failedPaymentNoticeEnabled` is on).
- A successful charge resets `failedPaymentCount`, `retryCount` and `nextRetryAt`.

### Payment Plans

- A declined installment is retried every `retryIntervalDays` (default **3** days) up to `maxRetryAttempts` (default **3**) attempts, while `retryFailedPayments` is `true`.
- While a retry is pending the installment status is `pending` with `nextRetryAt` set.
- After the last failed attempt the plan becomes `defaulted`.
- Late fees (if configured) keep accruing on overdue installments and are added to the retried charge. See [Payment Plans](./payment-plans.md).

> **Note:** The retry schedule and the action after the last retry are fixed by the platform. There are no dunning-settings endpoints, no configurable retry schedule, no grace period and no `cancel` / `manual_review` action.

---

## Failure Notice Setting

The only merchant-level dunning control is whether the failed-payment notice is sent. It is part of the merchant's messaging defaults.

### Get Messaging Defaults

```http
GET /api/v1/merchants/:merchantId/messaging-defaults
```

```bash
curl https://uat.auxcore.net/api/v1/merchants/c1a7e5d2-3b4f-4e6a-9c8d-7f0b1a2e3d45/messaging-defaults \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "successfulPaymentReceiptEnabled": true,
    "recurringPaymentReminderEnabled": true,
    "failedPaymentNoticeEnabled": true,
    "usingDefaults": true
  }
}
```

### Update Messaging Defaults

```http
PUT /api/v1/merchants/:merchantId/messaging-defaults
```

```bash
curl -X PUT https://uat.auxcore.net/api/v1/merchants/c1a7e5d2-3b4f-4e6a-9c8d-7f0b1a2e3d45/messaging-defaults \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id" \
  -H "Content-Type: application/json" \
  -d '{ "failedPaymentNoticeEnabled": false }'
```

All three flags are optional booleans; omitted flags keep their value. Admin roles can read and change any merchant; merchant users only their own merchant. Any other caller gets `403` (`{ "success": false, "message": "..." }`).

Setting `failedPaymentNoticeEnabled` to `false` suppresses the dunning email. Per-schedule notifications can also be off (`sendNotifications: false` on the schedule).

---

## Monitor Failed Payments

There is no dedicated failed-payments endpoint. Use these instead:

- `GET /api/v1/recurring/overview` — `data.failed` is the number of schedules with at least one failed payment.
- `GET /api/v1/recurring/:id` — `failedPaymentCount`, `retryCount`, `maxRetryAttempts`, `nextRetryAt`, `lastFailedPaymentDate`, `pausedReason`.
- `GET /api/v1/recurring/:id/schedule` — cycle rows with status `failed`.
- `GET /api/v1/billing/payment-plans?status=defaulted` — defaulted payment plans.

---

## Retry Failed Payment

Reset the retry counters of a subscription so the scheduler retries on its next run. This does **not** charge immediately; use Charge Now for that. Returns `400` "No failed payments to retry" when `failedPaymentCount` is 0.

### Endpoint

```http
POST /api/v1/recurring/:id/retry-failed
```

```bash
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/retry-failed \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61",
    "status": "active",
    "failedPaymentCount": 0,
    "retryCount": 0,
    "nextRetryAt": "2026-10-01T16:45:00.000Z"
  },
  "message": "Payment retry initiated successfully"
}
```

> **Note:** A schedule that was auto-paused must also be resumed (`POST /api/v1/recurring/:id/resume`) before the scheduler charges it again.

---

## Charge Now

Charge the next payment immediately.

### Endpoint

```http
POST /api/v1/recurring/:id/charge-now
```

**Response:**
```json
{
  "success": true,
  "data": { "success": true, "transactionId": "5b9e2d7a-1c3f-4a8e-b6d0-2f4c8a1e7b93" },
  "message": "Payment processed successfully"
}
```

A decline returns `400` with `error.code` `PROCESSING_ERROR`. A second attempt within 60 seconds is rejected. For payment plans use `POST /api/v1/billing/payment-plans/:id/charge-now` (decline → `402`).

---

## Update the Customer's Card

Most failures are fixed by a new card.

### Send Update Link

Emails the customer a secure link (valid 24 hours) to replace the card on file.

```http
POST /api/v1/recurring/:id/send-method-update-link
```

For payment plans: `POST /api/v1/billing/payment-plans/:id/send-method-update-link`.

### Link a New Card

Attach a card you collected yourself (PCI vault token). It becomes the default method; the card is not charged.

```http
POST /api/v1/recurring/:id/link-payment-method
```

```json
{ "pciToken": "pci_tok_1a2b3c4d5e6f", "last4": "4242", "brand": "visa" }
```

For payment plans: `POST /api/v1/billing/payment-plans/:id/link-payment-method`.

---

## Notifications

AuxCORE does not emit dunning webhook events. Each attempt is an ordinary transaction, so declined attempts appear as declined transactions. Customer emails:

| Email | When |
|-------|------|
| Payment failed | After each failed automatic charge of a subscription |
| Dunning notice | Once, when a subscription is paused after its last retry (unless `failedPaymentNoticeEnabled` is `false`) |
| Payment successful | After a successful charge, including a recovered one |

---

## Common Failure Reasons

| Reason | Common Cause | Customer Action |
|--------|--------------|-----------------|
| **Insufficient funds** | Not enough balance | Add funds to account |
| **Card declined** | Card issuer declined | Try different card |
| **Card expired** | Card past expiry date | Update card details |
| **Invalid card** | Card info incorrect | Verify card details |
| **Stop recurring** (codes 261/262) | Cardholder asked the issuer to stop | Contact the customer; do not retry |

---

## Best Practices

### ✅ DO:
- **Watch the overview** - Follow up on schedules with failed payments
- **Send the update link early** - Before the last retry runs
- **Resume after fixing** - Auto-paused schedules stay paused until resumed
- **Keep failure notices on** - Customers can only fix what they know about

### ❌ DON'T:
- **Don't hammer Charge Now** - Repeated declines can trigger issuer blocks
- **Don't retry stop-recurring declines** - The cardholder revoked the authorization
- **Don't cancel immediately** - A paused schedule can be resumed

---

## Testing Dunning

```bash
# 1. Create a schedule with a card that will decline in the Sandbox, due today
# 2. Charge it now and confirm the decline
curl -X POST https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61/charge-now \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"

# 3. Inspect the retry state
curl https://uat.auxcore.net/api/v1/recurring/8d2f1c4b-6a3e-4f7d-b1c9-0e5a7d3f2b61 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "X-Tenant-ID: your-tenant-id"
```

See [Testing](../getting-started/testing.md) for Sandbox decline cards.

---

## Next Steps

- [Create subscriptions →](./subscriptions.md)
- [Payment plans →](./payment-plans.md)
- [Handle webhooks →](../getting-started/webhooks.md)

---

**Need help?** Contact support@auxcore.net
