> ## 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.

# Cancellations

> List, inspect, and act on cancellation cases — apply retention offers, update churn reasons, and finalize cancellations from the Medusa Admin.

The cancellation case endpoints expose the Admin cancellation and retention queue. You can list open cases for DataTable views, fetch a full detail payload including offer history and subscription context, apply a retention offer (pause, discount, or bonus), update the churn reason, and finalize a case as cancelled. 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                                        |
| ---------------------- | ---------------------------------------------- |
| `requested`            | Customer has requested cancellation            |
| `evaluating_retention` | Case is open and under admin review            |
| `retention_offered`    | A retention offer has been proposed            |
| `retained`             | Customer accepted a retention offer            |
| `paused`               | Subscription was paused as a retention outcome |
| `canceled`             | Case is finalized as cancelled                 |

**Final outcome values:** `retained`, `paused`, `canceled`.

**Offer decision status values:** `proposed`, `accepted`, `rejected`, `applied`, `expired`.

**Reason category values:** `price`, `product_fit`, `delivery`, `billing`, `temporary_pause`, `switched_competitor`, `other`.

***

## GET /admin/cancellations

Returns the paginated cancellation 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: `created_at`, `updated_at`, `status`, `final_outcome`, `reason_category`, `finalized_at`. In-memory: `subscription_reference`, `customer_name`, `product_title`.
</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="final_outcome" type="string | string[]">
  Filter by final outcome. Accepts a single value or an array.
</ParamField>

<ParamField query="reason_category" type="string | string[]">
  Filter by churn reason category. Accepts a single value or an array.
</ParamField>

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

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

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

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

### Response

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

  <Expandable title="cancellation case properties">
    <ResponseField name="id" type="string">Cancellation case ID.</ResponseField>
    <ResponseField name="status" type="string">Current case status.</ResponseField>
    <ResponseField name="reason" type="string">Free-text churn reason.</ResponseField>
    <ResponseField name="reason_category" type="string">Normalized reason category.</ResponseField>
    <ResponseField name="final_outcome" type="string | null">Final outcome once the case is closed, or `null` while open.</ResponseField>
    <ResponseField name="subscription" type="object">Subscription summary including reference, status, customer name, product info, and renewal timestamps.</ResponseField>
    <ResponseField name="created_at" type="string">Case creation timestamp.</ResponseField>
    <ResponseField name="finalized_at" type="string | null">When the case was closed, or `null` while open.</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}
{
  "cancellations": [
    {
      "id": "cc_123",
      "status": "evaluating_retention",
      "reason": "Customer says the price is too high",
      "reason_category": "price",
      "final_outcome": null,
      "subscription": {
        "subscription_id": "sub_123",
        "reference": "SUB-001",
        "status": "active",
        "customer_name": "Jane Doe",
        "product_title": "Coffee Subscription",
        "variant_title": "1 kg",
        "sku": "COFFEE-1KG",
        "next_renewal_at": "2026-04-15T10:00:00.000Z",
        "last_renewal_at": "2026-03-15T10:00:00.000Z",
        "paused_at": null,
        "cancelled_at": null,
        "cancel_effective_at": null
      },
      "created_at": "2026-04-01T10:00:00.000Z",
      "finalized_at": null,
      "updated_at": "2026-04-01T10:05: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/cancellations/:id

Returns the full detail payload for a single cancellation case, including offer history, subscription context, and linked renewal or dunning summaries.

### Path parameters

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

### Response

<ResponseField name="cancellation" type="object" required>
  <Expandable title="detail fields">
    <ResponseField name="id" type="string">Cancellation case ID.</ResponseField>
    <ResponseField name="status" type="string">Current case status.</ResponseField>
    <ResponseField name="reason" type="string">Free-text churn reason.</ResponseField>
    <ResponseField name="reason_category" type="string">Normalized reason category.</ResponseField>
    <ResponseField name="final_outcome" type="string | null">Final outcome once closed.</ResponseField>
    <ResponseField name="subscription" type="object">Full subscription summary.</ResponseField>
    <ResponseField name="notes" type="string | null">Operator notes.</ResponseField>
    <ResponseField name="finalized_by" type="string | null">Actor ID who finalized the case.</ResponseField>
    <ResponseField name="cancellation_effective_at" type="string | null">When the cancellation takes effect.</ResponseField>
    <ResponseField name="dunning" type="object | null">Linked dunning summary, or `null`.</ResponseField>
    <ResponseField name="renewal" type="object | null">Linked renewal cycle summary, or `null`.</ResponseField>
    <ResponseField name="offers" type="object[]">Ordered list of retention offer events.</ResponseField>
    <ResponseField name="metadata" type="object | null">Operational metadata.</ResponseField>
    <ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
    <ResponseField name="finalized_at" type="string | null">Finalization timestamp, or `null`.</ResponseField>
    <ResponseField name="updated_at" type="string">Last update timestamp.</ResponseField>
  </Expandable>
</ResponseField>

```json Response example theme={null}
{
  "cancellation": {
    "id": "cc_123",
    "status": "retained",
    "reason": "Customer asked for a lower price",
    "reason_category": "price",
    "final_outcome": "retained",
    "subscription": {
      "subscription_id": "sub_123",
      "reference": "SUB-001",
      "status": "active",
      "customer_name": "Jane Doe",
      "product_title": "Coffee Subscription",
      "variant_title": "1 kg",
      "sku": "COFFEE-1KG",
      "next_renewal_at": "2026-04-15T10:00:00.000Z",
      "last_renewal_at": "2026-03-15T10:00:00.000Z",
      "paused_at": null,
      "cancelled_at": null,
      "cancel_effective_at": null
    },
    "created_at": "2026-04-01T10:00:00.000Z",
    "finalized_at": "2026-04-01T10:20:00.000Z",
    "updated_at": "2026-04-01T10:20:00.000Z",
    "notes": "Customer accepted a temporary retention discount",
    "finalized_by": "user_123",
    "cancellation_effective_at": null,
    "dunning": null,
    "renewal": {
      "renewal_cycle_id": "re_123",
      "status": "scheduled",
      "scheduled_for": "2026-04-15T10:00:00.000Z",
      "approval_status": null,
      "generated_order_id": null
    },
    "offers": [
      {
        "id": "roe_123",
        "offer_type": "discount_offer",
        "offer_payload": {
          "discount_offer": {
            "discount_type": "percentage",
            "discount_value": 10,
            "duration_cycles": 2,
            "note": null
          }
        },
        "decision_status": "applied",
        "decision_reason": "Customer accepted the offer",
        "decided_at": "2026-04-01T10:15:00.000Z",
        "decided_by": "user_123",
        "applied_at": "2026-04-01T10:15:00.000Z",
        "metadata": null,
        "created_at": "2026-04-01T10:15:00.000Z",
        "updated_at": "2026-04-01T10:15:00.000Z"
      }
    ],
    "metadata": {
      "manual_actions": []
    }
  }
}
```

### Errors

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

***

## POST /admin/cancellations/:id/apply-offer

Applies a retention action to an open case. Creates a `RetentionOfferEvent`, updates the subscription, and closes the case as `retained` or `paused`. Returns the refreshed detail payload.

### Path parameters

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

### Body parameters

<ParamField body="offer_type" type="string" required>
  Type of retention offer. One of `pause_offer`, `discount_offer`, or `bonus_offer`.
</ParamField>

<ParamField body="offer_payload" type="object" required>
  Offer-specific configuration. Shape depends on `offer_type` — see examples below.
</ParamField>

<ParamField body="decided_by" type="string" required>
  Actor ID of the admin user applying the offer.
</ParamField>

<ParamField body="decision_reason" type="string">
  Optional reason for applying the offer.
</ParamField>

```json Pause offer example theme={null}
{
  "offer_type": "pause_offer",
  "offer_payload": {
    "pause_offer": {
      "pause_cycles": 2,
      "resume_at": null,
      "note": "Customer wants a short break"
    }
  },
  "decided_by": "user_123",
  "decision_reason": "Pause accepted by customer"
}
```

```json Discount offer example theme={null}
{
  "offer_type": "discount_offer",
  "offer_payload": {
    "discount_offer": {
      "discount_type": "percentage",
      "discount_value": 10,
      "duration_cycles": 2,
      "note": "Temporary save offer"
    }
  },
  "decided_by": "user_123",
  "decision_reason": "Customer accepted a lower price"
}
```

```json Bonus offer example theme={null}
{
  "offer_type": "bonus_offer",
  "offer_payload": {
    "bonus_offer": {
      "bonus_type": "free_cycle",
      "value": 1,
      "label": null,
      "duration_cycles": 1,
      "note": null
    }
  },
  "decided_by": "user_123",
  "decision_reason": "Customer accepted a free cycle"
}
```

<Note>Validation rules: `pause_offer` requires `pause_cycles` or `resume_at`; percentage discounts cannot exceed `50`; `discount_value` must be positive; `duration_cycles` must be positive when provided; `free_cycle` and `credit` bonus types require `value`.</Note>

### Errors

| Code  | Error                 | Meaning                                       |
| ----- | --------------------- | --------------------------------------------- |
| `404` | `not_found`           | Cancellation case does not exist              |
| `409` | `invalid_state`       | Case is terminal or cannot accept a new offer |
| `409` | `offer_out_of_policy` | Offer payload violates retention policy rules |

***

## POST /admin/cancellations/:id/reason

Updates the churn reason, normalized category, and operator notes for an open case. Returns the refreshed detail payload.

### Path parameters

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

### Body parameters

<ParamField body="reason" type="string">
  Free-text churn reason.
</ParamField>

<ParamField body="reason_category" type="string">
  Normalized reason category.
</ParamField>

<ParamField body="notes" type="string">
  Operator notes.
</ParamField>

<ParamField body="updated_by" type="string">
  Actor ID of the admin user making the update.
</ParamField>

<ParamField body="update_reason" type="string">
  Reason for the classification change.
</ParamField>

```json Request example theme={null}
{
  "reason": "The subscription no longer fits the customer needs",
  "reason_category": "product_fit",
  "notes": "Customer wants to stop after current cycle",
  "updated_by": "user_123",
  "update_reason": "Operator clarified churn classification"
}
```

### Errors

| Code  | Error           | Meaning                              |
| ----- | --------------- | ------------------------------------ |
| `404` | `not_found`     | Cancellation case does not exist     |
| `409` | `invalid_state` | Case is terminal or cannot be edited |

***

## POST /admin/cancellations/:id/finalize

Finalizes a case as `canceled`, updates the subscription lifecycle, computes `cancel_effective_at`, and clears renewal eligibility. Returns the refreshed detail payload.

### Path parameters

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

### Body parameters

<ParamField body="reason" type="string">
  Churn reason for the final cancellation. Required by domain rules; if omitted, the workflow falls back to the existing case reason.
</ParamField>

<ParamField body="reason_category" type="string">
  Normalized reason category.
</ParamField>

<ParamField body="notes" type="string">
  Operator notes.
</ParamField>

<ParamField body="finalized_by" type="string">
  Actor ID of the admin user finalizing the case.
</ParamField>

<ParamField body="effective_at" type="string">
  When the cancellation takes effect. One of `immediately` or `end_of_cycle`.
</ParamField>

```json Request example theme={null}
{
  "reason": "Customer is switching to another provider",
  "reason_category": "switched_competitor",
  "notes": "No retention offer accepted",
  "finalized_by": "user_123",
  "effective_at": "immediately"
}
```

### Errors

| Code  | Error           | Meaning                                                       |
| ----- | --------------- | ------------------------------------------------------------- |
| `400` | `invalid_data`  | Reason is missing after resolving body and existing case data |
| `404` | `not_found`     | Cancellation case does not exist                              |
| `409` | `invalid_state` | Case is terminal or not eligible for final cancellation       |
