Skip to main content
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.
All routes require an authenticated Medusa Admin user. Unauthenticated requests return 401.

Status values

ValueMeaning
requestedCustomer has requested cancellation
evaluating_retentionCase is open and under admin review
retention_offeredA retention offer has been proposed
retainedCustomer accepted a retention offer
pausedSubscription was paused as a retention outcome
canceledCase 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

limit
number
Number of results per page.
offset
number
Zero-based result offset for pagination.
q
string
Free-text search across subscription reference, customer name, and product title.
order
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.
direction
string
Sort direction. One of asc or desc.
status
string | string[]
Filter by case status. Accepts a single value or an array.
final_outcome
string | string[]
Filter by final outcome. Accepts a single value or an array.
reason_category
string | string[]
Filter by churn reason category. Accepts a single value or an array.
offer_type
string | string[]
Filter by retention offer type. Accepts a single value or an array.
subscription_id
string
Filter to cases for a specific subscription.
created_from
string
ISO datetime lower bound for created_at.
created_to
string
ISO datetime upper bound for created_at.

Response

cancellations
object[]
required
Array of cancellation case list items.
count
number
required
Total matching records.
limit
number
required
Page size used.
offset
number
required
Result offset used.
Response example
{
  "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

CodeErrorMeaning
400invalid_dataInvalid 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

id
string
required
Cancellation case ID.

Response

cancellation
object
required
Response example
{
  "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

CodeErrorMeaning
404not_foundCancellation 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

id
string
required
Cancellation case ID.

Body parameters

offer_type
string
required
Type of retention offer. One of pause_offer, discount_offer, or bonus_offer.
offer_payload
object
required
Offer-specific configuration. Shape depends on offer_type — see examples below.
decided_by
string
required
Actor ID of the admin user applying the offer.
decision_reason
string
Optional reason for applying the offer.
Pause offer example
{
  "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"
}
Discount offer example
{
  "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"
}
Bonus offer example
{
  "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"
}
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.

Errors

CodeErrorMeaning
404not_foundCancellation case does not exist
409invalid_stateCase is terminal or cannot accept a new offer
409offer_out_of_policyOffer 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

id
string
required
Cancellation case ID.

Body parameters

reason
string
Free-text churn reason.
reason_category
string
Normalized reason category.
notes
string
Operator notes.
updated_by
string
Actor ID of the admin user making the update.
update_reason
string
Reason for the classification change.
Request example
{
  "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

CodeErrorMeaning
404not_foundCancellation case does not exist
409invalid_stateCase 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

id
string
required
Cancellation case ID.

Body parameters

reason
string
Churn reason for the final cancellation. Required by domain rules; if omitted, the workflow falls back to the existing case reason.
reason_category
string
Normalized reason category.
notes
string
Operator notes.
finalized_by
string
Actor ID of the admin user finalizing the case.
effective_at
string
When the cancellation takes effect. One of immediately or end_of_cycle.
Request example
{
  "reason": "Customer is switching to another provider",
  "reason_category": "switched_competitor",
  "notes": "No retention offer accepted",
  "finalized_by": "user_123",
  "effective_at": "immediately"
}

Errors

CodeErrorMeaning
400invalid_dataReason is missing after resolving body and existing case data
404not_foundCancellation case does not exist
409invalid_stateCase is terminal or not eligible for final cancellation