> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reorderjs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Dunning

> List, inspect, and act on dunning cases — retry payments, mark cases recovered or unrecovered, and override retry schedules from the Medusa Admin.

The dunning case endpoints expose the Admin dunning queue and let you manage individual payment recovery cases. You can list open or scheduled cases for DataTable views, fetch a full detail payload including attempt history and retry schedule, trigger an immediate retry, close a case manually as recovered or unrecovered, and override the retry schedule for a single case. All routes require an authenticated Medusa Admin user. All mutations are workflow-backed and return the refreshed detail payload.

<Info>All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`.</Info>

## Status values

| Value                        | Meaning                                                           |
| ---------------------------- | ----------------------------------------------------------------- |
| `open`                       | Case is newly opened                                              |
| `retry_scheduled`            | Last retry failed with a retryable error; next retry is scheduled |
| `retrying`                   | Retry is currently in flight                                      |
| `awaiting_manual_resolution` | Automatic retries are exhausted; manual action required           |
| `recovered`                  | Payment was collected; case is closed                             |
| `unrecovered`                | Case is closed as permanently unrecoverable                       |

**Attempt status values:** `processing`, `succeeded`, `failed`.

***

## GET /admin/dunning

Returns the paginated dunning queue for Admin DataTable views.

### Query parameters

<ParamField query="limit" type="number">
  Number of results per page.
</ParamField>

<ParamField query="offset" type="number">
  Zero-based result offset for pagination.
</ParamField>

<ParamField query="q" type="string">
  Free-text search across subscription reference, customer name, and product title.
</ParamField>

<ParamField query="order" type="string">
  Field to sort by. Database-backed: `updated_at`, `status`, `next_retry_at`, `attempt_count`, `max_attempts`, `last_attempt_at`. In-memory: `last_attempt_status`, `subscription_reference`, `customer_name`, `product_title`, `order_display_id`.
</ParamField>

<ParamField query="direction" type="string">
  Sort direction. One of `asc` or `desc`.
</ParamField>

<ParamField query="status" type="string | string[]">
  Filter by case status. Accepts a single value or an array.
</ParamField>

<ParamField query="subscription_id" type="string">
  Filter to cases for a specific subscription.
</ParamField>

<ParamField query="renewal_cycle_id" type="string">
  Filter by the originating renewal cycle ID.
</ParamField>

<ParamField query="renewal_order_id" type="string">
  Filter by the renewal order ID.
</ParamField>

<ParamField query="payment_provider_id" type="string">
  Filter by payment provider ID.
</ParamField>

<ParamField query="last_payment_error_code" type="string">
  Filter by the last payment error code (e.g. `card_declined`).
</ParamField>

<ParamField query="attempt_count_min" type="number">
  Minimum number of attempts filter.
</ParamField>

<ParamField query="attempt_count_max" type="number">
  Maximum number of attempts filter.
</ParamField>

<ParamField query="next_retry_from" type="string">
  ISO datetime lower bound for `next_retry_at`.
</ParamField>

<ParamField query="next_retry_to" type="string">
  ISO datetime upper bound for `next_retry_at`.
</ParamField>

<ParamField query="last_attempt_status" type="string | string[]">
  Filter by the status of the most recent attempt.
</ParamField>

### Response

<ResponseField name="dunning_cases" type="object[]" required>
  Array of dunning case list items.

  <Expandable title="dunning case properties">
    <ResponseField name="id" type="string">Dunning case ID.</ResponseField>
    <ResponseField name="status" type="string">Current case status.</ResponseField>
    <ResponseField name="subscription" type="object">Subscription summary including reference, status, customer name, product info, and payment provider ID.</ResponseField>
    <ResponseField name="renewal" type="object">Linked renewal cycle summary with `renewal_cycle_id`, `status`, `scheduled_for`, and `generated_order_id`.</ResponseField>
    <ResponseField name="order" type="object | null">Order summary with `order_id`, `display_id`, and `status`, or `null` if not yet generated.</ResponseField>
    <ResponseField name="attempt_count" type="number">Number of retry attempts made so far.</ResponseField>
    <ResponseField name="max_attempts" type="number">Maximum allowed retry attempts.</ResponseField>
    <ResponseField name="next_retry_at" type="string | null">Scheduled timestamp for the next automatic retry.</ResponseField>
    <ResponseField name="last_attempt_at" type="string | null">Timestamp of the most recent attempt.</ResponseField>
    <ResponseField name="last_payment_error_code" type="string | null">Error code from the last payment attempt.</ResponseField>
    <ResponseField name="updated_at" type="string">Last update timestamp.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="count" type="number" required>Total matching records.</ResponseField>
<ResponseField name="limit" type="number" required>Page size used.</ResponseField>
<ResponseField name="offset" type="number" required>Result offset used.</ResponseField>

```json Response example theme={null}
{
  "dunning_cases": [
    {
      "id": "dc_123",
      "status": "retry_scheduled",
      "subscription": {
        "subscription_id": "sub_123",
        "reference": "SUB-001",
        "status": "past_due",
        "customer_name": "Jane Doe",
        "product_title": "Coffee Subscription",
        "variant_title": "1 kg",
        "sku": "COFFEE-1KG",
        "payment_provider_id": "pp_stripe_stripe"
      },
      "renewal": {
        "renewal_cycle_id": "re_123",
        "status": "failed",
        "scheduled_for": "2026-04-15T10:00:00.000Z",
        "generated_order_id": "order_123"
      },
      "order": {
        "order_id": "order_123",
        "display_id": 1001,
        "status": "pending"
      },
      "attempt_count": 1,
      "max_attempts": 3,
      "next_retry_at": "2026-04-16T10:00:00.000Z",
      "last_attempt_at": "2026-04-15T10:02:00.000Z",
      "last_payment_error_code": "card_declined",
      "updated_at": "2026-04-15T10:02:00.000Z"
    }
  ],
  "count": 1,
  "limit": 20,
  "offset": 0
}
```

### Errors

| Code  | Error          | Meaning                                                                           |
| ----- | -------------- | --------------------------------------------------------------------------------- |
| `400` | `invalid_data` | Invalid query parameter shape, unsupported query value, or unsupported sort field |

***

## GET /admin/dunning/:id

Returns the full detail payload for a single dunning case, including the full attempt history and retry schedule.

### Path parameters

<ParamField path="id" type="string" required>
  Dunning case ID.
</ParamField>

### Response

Returns a `dunning_case` object with all list fields plus:

<ResponseField name="dunning_case" type="object" required>
  <Expandable title="additional detail fields">
    <ResponseField name="retry_schedule" type="object">Active retry policy with `strategy`, `intervals` (array of minutes), `timezone`, and `source`.</ResponseField>
    <ResponseField name="last_payment_error_message" type="string | null">Human-readable error message from the last payment attempt.</ResponseField>
    <ResponseField name="recovered_at" type="string | null">Timestamp when the case was marked recovered.</ResponseField>
    <ResponseField name="closed_at" type="string | null">Timestamp when the case was closed.</ResponseField>
    <ResponseField name="recovery_reason" type="string | null">Reason recorded when the case was closed.</ResponseField>
    <ResponseField name="attempts" type="object[]">Full attempt history with attempt number, status, timestamps, error code, error message, payment reference, and metadata.</ResponseField>
    <ResponseField name="metadata" type="object">Operational metadata such as `origin`.</ResponseField>
    <ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
  </Expandable>
</ResponseField>

```json Response example theme={null}
{
  "dunning_case": {
    "id": "dc_123",
    "status": "retry_scheduled",
    "subscription": {
      "subscription_id": "sub_123",
      "reference": "SUB-001",
      "status": "past_due",
      "customer_name": "Jane Doe",
      "product_title": "Coffee Subscription",
      "variant_title": "1 kg",
      "sku": "COFFEE-1KG",
      "payment_provider_id": "pp_stripe_stripe"
    },
    "renewal": {
      "renewal_cycle_id": "re_123",
      "status": "failed",
      "scheduled_for": "2026-04-15T10:00:00.000Z",
      "generated_order_id": "order_123"
    },
    "order": {
      "order_id": "order_123",
      "display_id": 1001,
      "status": "pending"
    },
    "attempt_count": 1,
    "max_attempts": 3,
    "retry_schedule": {
      "strategy": "fixed_intervals",
      "intervals": [1440, 4320, 10080],
      "timezone": "UTC",
      "source": "default_policy"
    },
    "next_retry_at": "2026-04-16T10:00:00.000Z",
    "last_payment_error_code": "card_declined",
    "last_payment_error_message": "Declined",
    "last_attempt_at": "2026-04-15T10:02:00.000Z",
    "recovered_at": null,
    "closed_at": null,
    "recovery_reason": null,
    "attempts": [
      {
        "id": "da_123",
        "attempt_no": 1,
        "status": "failed",
        "started_at": "2026-04-15T10:00:00.000Z",
        "finished_at": "2026-04-15T10:02:00.000Z",
        "error_code": "card_declined",
        "error_message": "Declined",
        "payment_reference": null,
        "metadata": null
      }
    ],
    "metadata": {
      "origin": "renewal_payment_failure"
    },
    "created_at": "2026-04-15T10:00:00.000Z",
    "updated_at": "2026-04-15T10:02:00.000Z"
  }
}
```

### Errors

| Code  | Error       | Meaning                     |
| ----- | ----------- | --------------------------- |
| `404` | `not_found` | Dunning case does not exist |

***

## POST /admin/dunning/:id/retry-now

Immediately runs the dunning payment retry workflow for a case, ignoring its scheduled `next_retry_at`. Retryable failures keep the case in `retry_scheduled`; permanent failures close it as `unrecovered`.

### Path parameters

<ParamField path="id" type="string" required>
  Dunning case ID.
</ParamField>

### Body parameters

<ParamField body="reason" type="string">
  Optional reason for the manual retry.
</ParamField>

```json Request example theme={null}
{
  "reason": "manual retry from admin"
}
```

### Errors

| Code  | Error       | Meaning                                                                                   |
| ----- | ----------- | ----------------------------------------------------------------------------------------- |
| `404` | `not_found` | Dunning case does not exist                                                               |
| `409` | `conflict`  | Retry is already processing, the case is terminal, or the transition is otherwise illegal |

***

## POST /admin/dunning/:id/mark-recovered

Closes a dunning case as recovered through a manual operator action. Use this when payment was collected outside the normal retry flow.

### Path parameters

<ParamField path="id" type="string" required>
  Dunning case ID.
</ParamField>

### Body parameters

<ParamField body="reason" type="string">
  Optional reason for the manual recovery.
</ParamField>

```json Request example theme={null}
{
  "reason": "paid outside normal retry flow"
}
```

### Errors

| Code  | Error       | Meaning                                                                 |
| ----- | ----------- | ----------------------------------------------------------------------- |
| `404` | `not_found` | Dunning case does not exist                                             |
| `409` | `conflict`  | Case is already recovered, already unrecovered, or a retry is in flight |

***

## POST /admin/dunning/:id/mark-unrecovered

Closes a dunning case as permanently unrecovered through a manual operator action. `reason` is required for this action.

### Path parameters

<ParamField path="id" type="string" required>
  Dunning case ID.
</ParamField>

### Body parameters

<ParamField body="reason" type="string" required>
  Reason for closing the case as unrecovered. Required.
</ParamField>

```json Request example theme={null}
{
  "reason": "customer refused to update payment method"
}
```

### Errors

| Code  | Error       | Meaning                                                                 |
| ----- | ----------- | ----------------------------------------------------------------------- |
| `404` | `not_found` | Dunning case does not exist                                             |
| `409` | `conflict`  | Case is already recovered, already unrecovered, or a retry is in flight |

***

## POST /admin/dunning/:id/retry-schedule

Overrides the retry policy for a single dunning case and updates future automatic retries. `max_attempts` must equal the number of entries in `intervals`.

### Path parameters

<ParamField path="id" type="string" required>
  Dunning case ID.
</ParamField>

### Body parameters

<ParamField body="intervals" type="number[]" required>
  Array of retry intervals in minutes. Each value must be a positive integer.
</ParamField>

<ParamField body="max_attempts" type="number" required>
  Maximum number of retry attempts. Must be a positive integer equal to the length of `intervals`.
</ParamField>

<ParamField body="reason" type="string">
  Optional reason for the schedule override.
</ParamField>

```json Request example theme={null}
{
  "reason": "short manual retry schedule",
  "intervals": [60, 120],
  "max_attempts": 2
}
```

### Errors

| Code  | Error          | Meaning                                                                                                       |
| ----- | -------------- | ------------------------------------------------------------------------------------------------------------- |
| `400` | `invalid_data` | Invalid payload shape, non-positive interval values, or `max_attempts` does not match the number of intervals |
| `404` | `not_found`    | Dunning case does not exist                                                                                   |
| `409` | `conflict`     | Case is terminal, a retry is in flight, or the override would create an illegal transition                    |
