# Activity Log Source: https://docs.reorderjs.com/admin/activity-log The activity log records subscription lifecycle events across all domains. Browse it globally or per subscription to see who changed what and when. The activity log records every significant lifecycle event across subscriptions, renewals, dunning, and cancellations — giving you a stable, chronological audit trail you can browse from two places: the global Activity Log page under **Subscriptions**, and the **Activity Log** section on each individual subscription's detail page. Both surfaces show who made a change, what changed, and when, with the ability to drill into a full event snapshot. ## The global activity log Navigate to **Subscriptions → Activity Log** to see events across all subscriptions in one place. ### Columns | Column | What it shows | | ---------------- | ---------------------------------------------------------------------------------------------------------------------- | | **Subscription** | The linked subscription reference | | **Created** | When the event was recorded | | **Actor** | Who or what triggered the event (admin user email, storefront customer, or `system`/`scheduler` for automated actions) | | **Event** | The event type badge | | **Reason** | The recorded reason for the event, if one exists | ### Search Use the search input to find events by subscription reference, customer name, or reason text. ### Filters Click **Add filter** to filter by **Event** type or **Actor**. Applied filters appear as chips in the toolbar. The page also exposes **Created from** and **Created to** date inputs. These are applied as filters but are not shown as separate chips — they appear inline in the toolbar area. ### Quick presets The toolbar includes quick presets to filter events by domain: | Preset | Events shown | | ----------------- | --------------------------------------------------------------------------------- | | **Subscriptions** | Subscription lifecycle events (created, pause, resume, cancel, plan change, etc.) | | **Renewals** | Renewal cycle events (scheduled, succeeded, failed, approved, etc.) | | **Dunning** | Dunning case events (opened, retry attempted, recovered, etc.) | | **Cancellation** | Cancellation case events (opened, offer applied, finalized, etc.) | Presets behave like other active filters and can be combined or removed individually. ### Sorting The list defaults to **Created descending** (newest events first). Use the sorting menu to change the sort order. ### Event detail Click any row to open an event detail drawer. The drawer shows: | Section | What it contains | | ------------------------- | --------------------------------------------------------- | | **Event overview** | Event type, actor, subscription reference, and timestamp | | **Subscription snapshot** | The subscription state at the time of the event | | **Changed fields** | The specific fields that changed, with old and new values | | **Previous state** | The full previous state snapshot | | **New state** | The full new state snapshot | | **Metadata** | Additional event context | For shipping address changes, the **Changed fields** section shows readable before/after address values rather than raw boolean flags. ## Event types The activity log captures events from four domains: ### Subscription events Events that record changes to a subscription record directly: * Subscription created * Status changed (paused, resumed, cancelled) * Plan change scheduled * Plan change applied * Shipping address updated * Next delivery skipped ### Renewal events Events generated during renewal cycle execution: * Renewal cycle scheduled * Renewal succeeded (order created) * Renewal failed * Approval requested * Changes approved or rejected * Force renewal triggered ### Dunning events Events from payment recovery cases: * Dunning case opened * Retry attempted * Retry succeeded or failed * Case marked recovered or unrecovered * Retry schedule overridden ### Cancellation events Events from cancellation and retention cases: * Cancellation case opened * Retention offer applied * Reason updated * Cancellation finalized ## Per-subscription event timeline The subscription detail page at `/app/subscriptions/:id` includes an **Activity Log** section that shows only events for that one subscription. This gives you immediate audit context without leaving the subscription detail view. If the subscription was created through storefront checkout, the timeline begins with a `Subscription created` event whose actor is the customer who placed the order. ### Timeline columns | Column | What it shows | | ----------- | ----------------------------------------------------------------- | | **Created** | When the event was recorded | | **Event** | The event type badge | | **Actor** | Who triggered the event (`admin` email, `system`, or `scheduler`) | | **Summary** | A readable summary of what changed | Entries are ordered newest first. The **Summary** column uses operator-facing labels — for example, an address change appears as a readable `old address → new address` diff rather than raw field names. ### Timeline filters The timeline supports domain and actor filters via **Add filter**. Date filters (**Created from** and **Created to**) are also available — add them from the **Add filter** menu and the corresponding date input appears below the toolbar. Removing the filter hides the input again. The timeline does not include a search input. ### Timeline event detail Click any timeline entry to open the event drawer with the full snapshot, including **Previous state**, **New state**, **Changed fields**, and **Metadata**. The drawer uses a dedicated query and does not reload the full timeline. Use the domain filter in the subscription timeline to focus on one category of events — for example, filter to **Renewals** to see just the billing history for that subscription. # Analytics Source: https://docs.reorderjs.com/admin/analytics Use the Analytics page to review subscription KPIs, inspect revenue and churn trends, track daily subscription creation, and export the view as CSV or JSON. The Analytics page at `/app/subscriptions/analytics` gives you a reporting-focused view of your subscription business. From a single screen you can check key metrics, visualize trends over a date range, narrow the data by product or billing cadence, and export the current view for offline analysis. Most filters apply to both the KPI cards and the trend chart. The exception is the **Created** trend tab, which always shows one bar per UTC day and uses only the selected date range. ## Navigating to Analytics In the Medusa Admin sidebar, go to **Subscriptions → Analytics**. ## Filtering the report The filter bar at the top of the page controls the analytics view. Changes to the date range, product, or cadence refresh the dashboard immediately. | Filter | Description | | ----------------------- | ----------------------------------------------------------------------------------------------------- | | **Date from / Date to** | The reporting date range | | **Status** | Subscription statuses to include (for example, `active`, `cancelled`) | | **Product** | Limit results to a specific product | | **Frequency** | Limit results to a specific billing cadence (for example, `month:1` for monthly, `year:1` for annual) | | **Group by** | Time bucket for `MRR`, `Churn`, and `LTV`: `day`, `week`, or `month` (defaults to `day`) | All date calculations use UTC. There is currently no timezone offset configuration for the Analytics page. ### Created tab exception When the **Created** tab is selected in the trend overview: * the chart always renders one bar per UTC day * `status`, `product`, `frequency`, and `group_by` do not affect that chart * the **Group by** control is disabled because the chart is fixed to daily buckets * days with no created subscriptions render as `0` ## KPI cards The page displays four KPI cards: | KPI | What it measures | | ------------------------ | ----------------------------------------------------------------- | | **MRR** | Monthly recurring revenue for the filtered dataset | | **Churn Rate** | Percentage of subscriptions that cancelled in the selected period | | **LTV** | Estimated lifetime value based on the filtered subscriptions | | **Active Subscriptions** | Count of active subscriptions matching the current filters | ### MRR and LTV null values **MRR** and **LTV** may display as empty or a fallback value in two cases: * The selected dataset contains subscriptions across multiple currencies, making a single aggregated revenue figure ambiguous. * The filtered dataset doesn't contain enough revenue data to calculate a meaningful value. Count-based metrics like **Active Subscriptions** always display regardless of currency composition. Percentage metrics like **Churn Rate** use the precision configured in the analytics response. ## Trend chart Below the KPI cards, the trend chart visualizes one metric over the selected date range. * **MRR**, **Churn**, and **LTV** use the selected **Group by** setting. * **Created** uses a dedicated bar chart with one bar per UTC day. * Hovering a created-subscription bar shows the exact day and the number of subscriptions created on that day. Buckets follow UTC date boundaries for all views. ## Exporting data Click **Export** to download the current analytics view. You can export as **CSV** or **JSON**. | Format | Use case | | ------ | ----------------------------------------- | | CSV | Spreadsheet analysis, BI tool import | | JSON | Programmatic processing, API-style output | Export behavior: * The export always reflects the currently active filters — no separate export configuration is needed. * Export is on demand and runs synchronously. The download starts immediately. * Column order and field names in the export follow the backend's deterministic schema. * Exporting does not reload or reset the KPI and trend data on screen. Set your date range and filters before exporting so the downloaded file matches exactly what you see in the KPI cards and chart. ## Rebuilding historical data If your analytics data looks incomplete for a historical range — for example, after a data correction or a fresh plugin installation — you can trigger a manual rebuild. Use the rebuild action available on the Analytics page to recalculate analytics snapshots for the specified date range. Rebuilding analytics for a large historical range can take time. The KPI cards and chart will reflect the rebuilt data once the rebuild completes. # Cancellation & Retention Source: https://docs.reorderjs.com/admin/cancellation-retention Use the Cancellations queue to review churn cases, apply retention offers like pauses or discounts, update cancellation reasons, and finalize each case. When a customer requests to cancel a subscription — or when your configured cancellation behavior routes the request through the retention flow — Reorder creates a cancellation case. The Cancellations queue at `/app/subscriptions/cancellations` is your workspace for reviewing these cases, applying retention offers to win customers back, or finalizing the cancellation when no retention is appropriate. Each case captures the churn reason, any retention offers you've tried, and the final outcome. ## The cancellations queue Navigate to **Subscriptions → Cancellations** to open the queue. ### Columns | Column | What it shows | | ------------------- | ------------------------------------------------------------------------ | | **Subscription** | The linked subscription reference and customer | | **Reason category** | The classified churn reason | | **Outcome** | The current outcome of the case (retained, paused, canceled, or pending) | | **Created** | When the cancellation case was opened | Click any row to open the detail page for that case. ### Search Use the search input to find cases by subscription reference, customer name, product title, variant title, or churn reason text. ### Filters and date inputs Click **Add filter** to filter by: | Filter | Description | | ------------------- | --------------------------------------------- | | **Reason category** | Filter by the classified churn reason | | **Outcome** | Filter by case outcome | | **Offer type** | Filter by the type of retention offer applied | Active non-date filters appear as chips in the toolbar. The queue also exposes **Created from** and **Created to** date inputs that default to a ±30-day window around today. These inputs are not shown as filter chips. ### Sorting Use the sorting menu to sort by **Created**, **Status**, **Outcome**, or **Reason category**. ## Cancellation case detail Click a row in the queue to open the detail page. The page shows: * **Case overview** — status, outcome, churn reason, and creation date * **Subscription summary** — a linked card to the subscription record * **Dunning summary** — linked dunning context if the cancellation followed a dunning case * **Renewal summary** — linked renewal cycle if applicable * **Decision timeline** — a chronological record of decisions and status changes * **Offer history** — all retention offers applied to this case and their outcomes * **Case metadata** — additional context fields recorded with the case ### Case statuses and outcomes | Status / Outcome | What it means | | ---------------- | ------------------------------------------------------------- | | Active | The case is open and awaiting a decision | | `retained` | A retention offer was accepted and the subscription continues | | `paused` | A pause offer was applied and the subscription is on hold | | `canceled` | The cancellation was finalized and the subscription ended | Cases with a terminal outcome (`retained`, `paused`, `canceled`) are read-only — no further actions are available. ## Applying a retention offer Use **Apply retention offer** to offer the customer an incentive to stay. You can apply a pause, a discount, or a bonus. Click the cancellation case row in the queue to open the detail page. From the action menu, select **Apply retention offer**. A drawer opens. Select one of the three offer types: * **Pause** — temporarily pauses the subscription for a set number of cycles or until a resume date * **Discount** — applies a percentage or fixed discount for a defined number of cycles * **Bonus** — grants a bonus (such as a free item or credit) for a defined number of cycles Complete the fields for your chosen offer type: **Pause offer** * Pause cycles * Resume at (date) * Decision reason * Notes (optional) **Discount offer** * Discount type (percentage or fixed) * Discount value * Duration (cycles) * Decision reason * Notes (optional) **Bonus offer** * Bonus type * Value * Label * Duration (cycles) * Decision reason * Notes (optional) Submit the drawer. A confirmation prompt appears before the offer is applied — pause offers show a stronger warning because they change the subscription lifecycle state. The applied offer appears in the **Offer history** section of the detail page. Applying a retention offer does not automatically finalize the case. You can apply multiple offers before closing a case. Use **Finalize cancellation** when no further retention attempts are appropriate. ## Updating the cancellation reason Use **Update reason** to correct or reclassify the churn reason on an active case. Click the cancellation case row to open the detail page. From the action menu, select **Update reason**. A drawer opens pre-filled with the current reason fields. Edit the **Reason**, **Reason category**, and **Notes** as needed. Click **Save**. The case updates immediately. ## Finalizing a cancellation Use **Finalize cancellation** when you've determined that retention isn't possible or appropriate, and you want to formally end the subscription. Click the cancellation case row to open the detail page. From the action menu, select **Finalize cancellation**. A drawer opens. Enter the **Reason**, **Reason category**, and any **Notes**. Optionally set an **Effective at** date if you want the cancellation to take effect at a specific time. Submit the drawer. A final confirmation prompt appears that explains the lifecycle impact — the subscription will be cancelled and no further renewals will run. Finalizing a cancellation ends the subscription permanently. This action cannot be undone. # Dunning Source: https://docs.reorderjs.com/admin/dunning Use the Dunning queue to monitor payment recovery cases, retry payments, override retry schedules, and manually close cases as recovered or unrecovered. When a renewal payment fails, Reorder creates a dunning case and begins automatically retrying the payment according to the retry schedule configured in **Settings → Subscription Settings**. The Dunning queue at `/app/subscriptions/dunning` gives you a live view of every active and historical dunning case. From here you can inspect the retry history, trigger an immediate retry, override the retry schedule for a specific case, or manually close a case as recovered or unrecovered. ## The dunning queue Navigate to **Subscriptions → Dunning** to open the queue. ### Columns | Column | What it shows | | ---------------- | ---------------------------------------------- | | **Subscription** | The linked subscription reference and customer | | **Status** | Current recovery state of the case | | **Next retry** | When the next automatic retry is scheduled | | **Attempts** | Total number of retry attempts made | | **Last error** | The most recent payment error code or message | Click any row to open the detail page for that case. ### Search Use the search input to find cases by subscription reference, customer name, product title, variant title, SKU, payment provider, or last payment error code. ### Filters and inputs Click **Add filter** to filter by **Status**. Applied status filters appear as chips in the toolbar. The queue also exposes dedicated inputs that let you narrow results further: | Input | Description | | ------------------------ | -------------------------------------------------------------------- | | **Provider ID** | Filter by payment provider | | **Error code** | Filter by the payment error code | | **Attempts min / max** | Filter by number of retry attempts | | **Next retry from / to** | Filter by the scheduled next-retry date range (defaults to ±30 days) | ### Sorting Use the sorting menu to sort by **Status**, **Next retry**, **Attempts**, or **Last attempt**. ## Dunning case statuses | Status | What it means | | ---------------------------- | ---------------------------------------------------------------------------------------------- | | `open` | The case was just created and no retry has run yet | | `retry_scheduled` | The last retry failed with a retryable error and a future retry is scheduled | | `retrying` | A retry is currently in progress | | `awaiting_manual_resolution` | Automatic retries cannot continue — a manual decision is required | | `recovered` | The payment was collected and the subscription returned to `active` | | `unrecovered` | The case is closed without payment recovery — retries exhausted or a terminal failure occurred | ## Dunning case detail Click a row in the queue to open the detail page. The detail page is split into two columns: * **Left column** — Case overview, payment summary, retry schedule, attempt timeline, and technical metadata * **Right column** — Linked subscription card, linked renewal cycle card, and linked order / payment card (where records exist) ### Detail actions The action menu in the top-right of the detail page provides the following actions: | Action | When it's available | | ----------------------- | ------------------------------------------------------------------ | | **Retry now** | Case is open or retry\_scheduled; blocked during in-flight retries | | **Mark recovered** | Case is not yet terminal; blocked during in-flight retries | | **Mark unrecovered** | Case is not yet terminal; blocked during in-flight retries | | **Edit retry schedule** | Case is not yet terminal; blocked during in-flight retries | Actions are disabled while a mutation is in progress. ## Retrying a payment immediately Use **Retry now** to attempt payment recovery without waiting for the next scheduled retry. Click the dunning case row in the queue to open the detail page. From the action menu, select **Retry now**. A confirmation prompt appears. Confirm the action. Reorder immediately attempts to charge the payment method. Retrying now triggers an immediate payment attempt. Make sure the customer's payment method is in a state that can succeed before proceeding. ## Marking a case recovered Use **Mark recovered** to manually close a dunning case when payment has been handled outside of the automatic retry flow — for example, after resolving an issue directly with your payment provider. Click the dunning case row in the queue to open the detail page. From the action menu, select **Mark recovered**. A confirmation prompt appears. Confirm the action. The case is closed as `recovered` and the linked subscription returns to `active`. ## Marking a case unrecovered Use **Mark unrecovered** to close a dunning case when you've determined the payment cannot be collected. Click the dunning case row in the queue to open the detail page. From the action menu, select **Mark unrecovered**. A confirmation prompt appears. Confirm the action. The case is closed as `unrecovered`. ## Overriding the retry schedule Use **Edit retry schedule** to change the retry intervals or maximum attempts for one specific dunning case without changing the global settings. Click the dunning case row in the queue to open the detail page. From the action menu, select **Edit retry schedule**. A drawer opens with the current schedule pre-filled. Adjust the **Intervals** (in minutes) and **Max attempts** for this case. The drawer shows a warning if the override creates a risky schedule configuration. Enter an optional reason for the override to capture context in the audit trail. A final confirmation prompt appears before the override is saved. Retry schedule overrides apply only to the specific dunning case you're editing. The global retry policy in **Settings → Subscription Settings** is not affected. # Orders Source: https://docs.reorderjs.com/admin/orders Subscription context surfaced directly on Medusa order details pages. Reorder adds a widget to the native Medusa Admin **Order Details** page. The widget shows the subscription that produced the order — whether it was the initial checkout order or a renewal. ## Where It Appears The widget is injected into the `order.details` zone. In Medusa v2.17.2 and newer, administrators can customize the placement, section, and ordering of dashboard widgets using Medusa Admin's built-in **Layout Composer** (Editor view). It renders as a subscription widget when the order is linked to a subscription. For regular non-subscription orders, the widget shows a one-time order state instead of subscription data. ## What the Widget Shows The Reorder subscription ID (e.g. `sub_01J...`). Displayed as a clickable link that navigates to the full subscription detail page in Reorder's Admin UI. The current subscription status at the time the page loads. Color-coded: * **Green** — active * **Orange** — paused * **Grey** — cancelled * **Red** — payment failed / in dunning The billing cadence of the subscription (e.g. monthly, yearly, or a custom interval). The subscription discount label derived from the subscription's `pricing_snapshot`. This reflects the configured subscription offer context rather than a generic order-discount label. The projected next renewal date shown by Reorder for the linked subscription. ## Navigation Clicking the subscription reference in the widget takes you directly to that subscription's detail page in the Reorder Admin UI. From there you can review the full renewal history, trigger manual renewals, manage dunning, or cancel with a retention offer. ## Renewal Orders Each automated renewal creates a new Medusa order. That renewal order will also show the widget, linking back to the same parent subscription. This makes it straightforward to trace any order — initial or renewal — back to the subscription that generated it. # Plans & Offers Source: https://docs.reorderjs.com/admin/plans-offers Use the Plans & Offers page to create, edit, enable, and disable the subscription offer configurations that control which products are subscribable. The Plans & Offers page gives you a single workspace for configuring which products and variants customers can subscribe to, what billing frequencies are available, and what discounts or trial terms apply. Navigate to **Subscriptions → Plans & Offers** in the Medusa Admin sidebar to get started. Each offer record can target an entire product or a single variant — when both exist, the variant-level offer takes precedence. The **Effective source** column lets you see at a glance which record is currently active for any given target. ## The plans and offers list The list shows all configured offer records in a searchable, filterable table. ### Columns | Column | What it shows | | -------------------- | -------------------------------------------------------------------------------------------- | | **Name** | The offer name you assigned | | **Target** | The product or variant this offer applies to | | **Status** | Whether the offer is enabled or disabled | | **Frequencies** | The billing cadences configured for this offer | | **Effective source** | Which record is currently winning the config resolution: `Product`, `Variant`, or `Inactive` | | **Updated** | When the offer was last modified | The **Effective source** column is particularly useful when a variant-level offer is disabled and a product-level fallback is still active, or when no active offer exists for a target at all. ### Search Use the search input in the toolbar to find offers by offer name, product title, variant title, or SKU. ### Filters Click **Add filter** to apply one or more of the following: | Filter | Description | | ------------------ | ----------------------------------------------- | | **Status** | Filter by enabled or disabled | | **Scope** | Filter by product-level or variant-level offers | | **Frequency** | Filter by a specific billing cadence | | **Discount range** | Filter by discount percentage or amount | | **Product** | Select a product using a searchable picker | | **Variant** | Select a variant scoped to the chosen product | The **Product** and **Variant** filters open structured picker modals instead of free-text inputs, so you always select from real catalog data. Active filters appear as chips in the toolbar. Click **Clear all** to remove all filters at once. ### Sorting Use the sorting menu to sort the list by **Name**, **Status**, **Product**, or **Updated**. ### Row actions Each row has an action menu with the following options: | Action | What it does | | ----------- | --------------------------------------------- | | **Edit** | Opens the offer in an edit drawer | | **Enable** | Activates the offer (requires confirmation) | | **Disable** | Deactivates the offer (requires confirmation) | While an enable or disable mutation is in progress, the row action label updates to **Enabling...** or **Disabling...** and repeat actions on that row are blocked. ## Creating a new offer Click **Create** in the page header to open the create flow in a full-screen modal. Enter a name that helps you identify the offer in the list — for example, "Monthly coffee subscription" or "Annual plan – Starter". Choose whether this offer applies to a **Product** or a specific **Variant**. Use the product picker to search for and select the target product. If the scope is **Variant**, a second picker lets you select the specific variant within that product. Toggle **Offer enabled** on to make the offer active immediately after saving, or leave it off to save in draft state. Add one or more billing frequencies. Each frequency row has: * **Interval** — `week`, `month`, or `year` * **Value** — a positive integer (for example, `1` for monthly, `3` for quarterly) * **Discount** (optional) — a percentage or fixed discount amount for customers who choose this cadence Use **Add frequency** to add more rows. To remove a row, click the remove button on that row and confirm. Expand the **Rules** section to set: * **Minimum cycles** (optional) — the minimum number of billing cycles before a customer can cancel; leave empty if there is no minimum subscription period * **Trial** — toggle on to enable a free trial, then set the number of trial days * **Stacking policy** — how the subscription discount interacts with other store discounts (`allowed`, `disallow_all`, or `disallow_subscription_discounts`) Click **Create** in the modal header. The new offer appears in the list and the table refreshes automatically. Each `interval:value` combination must be unique within a single offer. Duplicate frequencies are rejected before the form can be submitted. ## Editing an existing offer Click **Edit** in a row's action menu to open the edit drawer. The drawer pre-fills all current values from the offer record. ### What you can change | Field | Editable | | --------------------------------------- | -------- | | Name | Yes | | Enabled state | Yes | | Frequencies and discounts | Yes | | Rules (minimum cycles, trial, stacking) | Yes | | Target product or variant | No | The target product and variant are read-only in the edit drawer. To retarget an offer, create a new one for the intended product or variant. The drawer loads the offer's current data when it opens. Changes you save are applied immediately and both the list and the drawer reflect the updated record. ## Enabling and disabling offers Use **Enable** and **Disable** in the row action menu to toggle an offer's active state without editing it. Disabling an offer prevents new subscriptions from using that configuration but does not affect subscriptions that are already active. ## How effective config resolution works When Reorder resolves which offer configuration applies to a product and variant — for example, during a plan change or a storefront offer lookup — it follows this order: 1. Look for an active variant-scoped offer for the requested variant. 2. If none exists, look for an active product-scoped offer for the product. 3. If neither is active, the product is not subscribable. The **Effective source** column in the list reflects this resolution. If a variant-level offer is disabled and a product-level offer is active, the column shows **Product**. If no active offer exists for the target context, it shows **Inactive**. # Renewals Source: https://docs.reorderjs.com/admin/renewals Use the Renewals queue to monitor scheduled and failed billing cycles, review pending plan changes, approve or reject them, and force renewals when needed. The Renewals queue at `/app/subscriptions/renewals` gives you a real-time view of every billing cycle Reorder has scheduled or attempted. You can see which cycles are waiting to run, which have failed, and which are blocked on a pending plan-change approval. From the queue you can click into any cycle's detail page to review the full attempt history, approve or reject a pending change, or force the cycle to run immediately. ## The renewals queue Navigate to **Subscriptions → Renewals** to open the queue. ### Columns | Column | What it shows | | ---------------- | ------------------------------------------------------- | | **Scheduled** | The date and time the renewal cycle is due to run | | **Subscription** | The linked subscription reference and customer | | **Status** | Current execution state of the cycle | | **Approval** | Whether this cycle is waiting on a plan-change approval | | **Last attempt** | When the most recent execution attempt ran | Click any row to open the detail page for that cycle. ### Search Use the search input to find cycles by subscription reference, customer name, product title, variant title, or SKU. ### Filters and date range Click **Add filter** to filter by **Status**, **Approval**, or **Last attempt**. Active filters appear as chips in the toolbar. The queue also exposes **Scheduled from** and **Scheduled to** date inputs that default to a ±30-day window around today. Adjust these inputs to widen or narrow the visible date range. These date inputs are not shown as filter chips. ### Sorting Use the sorting menu to sort by **Scheduled**, **Subscription**, **Status**, **Approval**, or **Last attempt**. ## Renewal cycle statuses | Status | What it means | | ------------ | -------------------------------------------------------------------------- | | `scheduled` | The cycle is queued and will run at the scheduled time | | `processing` | The cycle is currently executing | | `succeeded` | The renewal completed successfully and an order was created | | `failed` | The execution attempt failed; the cycle may be retried or moved to dunning | | `skipped` | The cycle was skipped because the subscription had skip-next-cycle enabled | ## Approval statuses | Approval status | What it means | | --------------- | ----------------------------------------------------------------------- | | `not_required` | No approval is needed for this cycle | | `pending` | A pending plan change requires operator approval before the cycle runs | | `approved` | The pending change was approved and the cycle will apply it | | `rejected` | The pending change was rejected; the cycle will run without applying it | Whether approval is required depends on the **Renewal behavior** setting in **Settings → Subscription Settings**. When set to **Require review for pending changes**, any cycle that includes a pending plan change will wait for operator approval. ## Renewal cycle detail Click a row in the queue to open the detail page. The detail page is split into two columns: * **Left column** — Cycle overview, approval summary, pending changes, attempt history, and technical metadata * **Right column** — Linked subscription card and generated order card (when the cycle produced an order) ### Detail actions The action menu in the top-right of the detail page provides the following actions: | Action | When it's available | | ------------------- | ----------------------------------------------------- | | **Force renewal** | Cycle status is `scheduled` or `failed` | | **Approve changes** | Approval is required and approval status is `pending` | | **Reject changes** | Approval is required and approval status is `pending` | Actions are disabled while a mutation is in progress. ## Approving a pending plan change When a renewal cycle is blocked on a pending plan change, you can approve it to let the cycle apply the change. Click the renewal cycle row in the queue to open the detail page. From the action menu, select **Approve changes**. A drawer opens showing the pending change details. Enter an optional reason for your approval decision. Submit the drawer. A confirmation prompt appears before the change is saved. ## Rejecting a pending plan change If you want the renewal to run without applying the pending change, reject it instead. Click the renewal cycle row in the queue to open the detail page. From the action menu, select **Reject changes**. A drawer opens. A reason is required before you can submit a rejection. Submit the drawer. A confirmation prompt appears before the rejection is saved. ## Forcing a renewal Use **Force renewal** to manually trigger execution of a cycle that is scheduled or has previously failed. Click the renewal cycle row in the queue to open the detail page. From the action menu, select **Force renewal**. A confirmation prompt appears. Confirm the action. Reorder immediately attempts to execute the cycle. Forcing a renewal triggers an immediate payment attempt and order creation. Confirm that the subscription and payment details are correct before proceeding. # Subscriptions Source: https://docs.reorderjs.com/admin/subscriptions Browse, filter, and act on subscriptions from the dedicated Subscriptions page, and manage lifecycle actions directly from each subscription's detail view. The Subscriptions page at `/app/subscriptions` is your main workspace for managing recurring customers. From here you can find any subscription by searching or filtering, inspect its full detail, and trigger lifecycle actions like pause, resume, or cancel. The same plugin also adds a subscription widget to standard Medusa order detail pages, so you can always navigate from an order to the linked subscription without leaving context. ## The subscriptions list Navigate to **Subscriptions** in the Medusa Admin sidebar to open the list. The table shows one row per subscription and is built with Medusa's standard `DataTable`, so it behaves consistently with other Admin list pages. ### Columns | Column | What it shows | | ---------------- | --------------------------------------------------------------- | | **Reference** | Human-readable subscription identifier (for example, `SUB-001`) | | **Product** | Product and variant name | | **Status** | Current lifecycle state with a status badge | | **Frequency** | Billing cadence (for example, every month) | | **Next renewal** | The projected next renewal date | ### Search Use the search input in the top-right of the toolbar to find subscriptions by subscription reference, customer name, customer email, product title, variant title, or SKU. ### Filters Click **Add filter** to apply one or more of the following filters: | Filter | Description | | ------------------- | -------------------------------------------------------- | | **Status** | Filter by `active`, `paused`, `past_due`, or `cancelled` | | **Trial** | Show only subscriptions currently in a trial period | | **Skip next cycle** | Show only subscriptions with the next delivery skipped | | **Next renewal** | Filter by a scheduled renewal date range | Active filters appear as chips in the toolbar. Remove them individually or click **Clear all** to reset. ### Sorting Use the sorting menu in the toolbar to sort by **Status**, **Product**, or **Next renewal**. ### Row actions Each row has an action menu with quick lifecycle actions. Available actions depend on the subscription's current status: | Status | Available actions | | ----------- | -------------------- | | `active` | Pause, Cancel | | `paused` | Resume, Cancel | | `cancelled` | No actions available | Click anywhere on a row to open that subscription's detail page. ## Subscription detail The detail page at `/app/subscriptions/:id` is the main operational screen for a single subscription. It shows the subscription reference, status badge, and a header action menu on the right. ### Information sections The detail page is organized into the following sections: * **Subscription** — billing frequency, dates, and skip status * **Customer** — linked customer record * **Product** — the subscribed variant, with a link to the variant detail page and SKU visible as a helper field * **Shipping address** — the address snapshot used for this subscription's deliveries * **Payment method** — the saved card or payment method used for recurring renewals * **Pending plan change** — any scheduled update to the plan or cadence that will apply at the next eligible renewal ### Detail actions Open the action menu in the top-right of the detail page to access the following: | Action | When it's available | | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | **Pause** | Subscription is `active` | | **Resume** | Subscription is `paused` | | **Schedule plan change** | Subscription is `active` or `paused` | | **Edit shipping address** | Subscription is `active` or `paused` | | **Change payment method** | Subscription is `active`, `paused`, or `past_due` | | **Cancel** | Subscription is `active`, `paused`, or `past_due` | | Destructive actions require confirmation before the change is applied. Actions are disabled while a mutation is in progress. | | **Schedule plan change** is only available on the detail page, not in the list row menu. Because it opens a form that requires context about the current plan, it follows the Medusa pattern of keeping form-based edits in detail views. ### Scheduling a plan change Use **Schedule plan change** to queue a future update to the subscribed variant or billing cadence. From the detail page action menu, select **Schedule plan change**. The drawer loads available variants for the product. Choose the variant the subscription should switch to at the next eligible renewal. Set the frequency interval and value for the updated cadence. Choose an effective date if you want the change to apply at a specific renewal rather than the next one. Click **Save**. The pending change appears in the **Pending plan change** section of the detail page until the next renewal applies it. ### Editing the shipping address Use **Edit shipping address** to update the address snapshot used for this subscription's deliveries. From the detail page action menu, select **Edit shipping address**. The drawer pre-fills with the current address. Edit any of the available fields: first name, last name, company, address line 1, address line 2, city, postal code, province/state, country, and phone. Click **Save**. The address change is recorded in the activity log with a readable before/after diff. ### Changing the payment method Use **Change payment method** to switch the saved card or payment instrument used for recurring renewals. From the detail page action menu or the Payment method section, select **Change payment method**. The drawer loads saved payment methods for the customer from the configured payment provider. Choose an available saved payment method from the list. The current payment method is highlighted with a **(current)** badge. Click **Save**. The subscription's payment context is updated, and a `subscription.payment_method_updated` event is written to the activity log. ## The subscription widget on order detail pages The Reorder plugin extends the standard Medusa **Order detail** page with a **Subscription** widget. The widget shows: * A linked subscription card when the order is associated with a subscription, with a direct link to the subscription detail page. * The current subscription discount label derived from the subscription's `pricing_snapshot`. * The projected next renewal date for the linked subscription. * A **One-time order** label when the order has no subscription link. This lets you move from any subscription-generated order to its parent subscription without navigating away from the order context. ## Status reference | Status | What it means for operators | | ----------- | --------------------------------------------------------------------------------------------- | | `active` | Billing is running normally. Pause, cancel, or schedule changes are available. | | `paused` | Billing is on hold. Resume or cancel to change state. | | `past_due` | A renewal payment failed and dunning is active. See [Managing dunning cases](/admin/dunning). | | `cancelled` | The subscription has ended. The detail page is read-only; no further actions are available. | # Activity Log Source: https://docs.reorderjs.com/api/admin-activity-log Read the global subscription activity log or the per-subscription event timeline — with filtering, sorting, and full state-diff payloads. The activity log endpoints give you a tamper-evident audit trail for all subscription lifecycle events. You can query the global log for DataTable views, fetch the full state-diff payload for a single event, or retrieve the ordered timeline scoped to one subscription. All routes are read-only, require an authenticated Medusa Admin user, and default to `created_at desc` ordering. All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`. ## Actor type values | Value | Meaning | | ----------- | -------------------------------------------- | | `user` | Medusa Admin user | | `customer` | Storefront customer who triggered the action | | `system` | Internal system action | | `scheduler` | Scheduled job | ## Event type groups Events are namespaced by domain: `subscription.*`, `renewal.*`, `dunning.*`, `cancellation.*`. The `subscription.*` group includes the initial `subscription.created` event written when a subscription is first created through storefront checkout. *** ## GET /admin/subscription-logs Returns the paginated global activity log for Admin DataTable views. ### Query parameters Number of results per page. Zero-based result offset for pagination. Free-text search. Field to sort by. Database-backed: `created_at`, `event_type`, `actor_type`. In-memory: `subscription_reference`, `customer_name`, `reason`. Sort direction. One of `asc` or `desc`. Defaults to `desc`. Filter to events for a specific subscription. Filter to events for a specific customer. Filter by event type. Accepts a single value or an array. Filter by actor type. Accepts a single value or an array. ISO datetime lower bound for `created_at`. ISO datetime upper bound for `created_at`. ### Response Array of activity log list items. Log record ID. ID of the linked subscription. Namespaced event type (e.g. `subscription.paused` or `subscription.created`). Type of actor that triggered the event. Raw actor ID, or `null` for system/scheduler actors. Enriched actor summary with `type`, `id`, `email`, `name`, and `display`. Subscription summary with reference, customer name, product info, and variant title. Reason recorded with the event, or `null`. Comma-separated list of changed field names. Event timestamp. Total matching records. Page size used. Result offset used. ```json Response example theme={null} { "subscription_logs": [ { "id": "slog_123", "subscription_id": "sub_123", "event_type": "subscription.created", "actor_type": "customer", "actor_id": "cus_123", "actor": { "type": "customer", "id": "cus_123", "email": null, "name": "Jane Doe", "display": "Jane Doe" }, "subscription": { "subscription_id": "sub_123", "reference": "SUB-001", "customer_id": "cus_123", "customer_name": "Jane Doe", "product_title": "Coffee Subscription", "variant_title": "1 kg" }, "reason": null, "change_summary": "subscription_created", "created_at": "2026-04-15T10:00:00.000Z" } ], "count": 1, "limit": 20, "offset": 0 } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------------------- | | `400` | `invalid_data` | Invalid query parameter shape or unsupported sort field | *** ## GET /admin/subscription-logs/:id Returns the full detail payload for a single activity log event, including the previous state, new state, and a field-level diff. ### Path parameters Activity log record ID. ### Response Returns a `subscription_log` object with all list fields plus: State snapshot before the event. State snapshot after the event. Field-level diff. Each entry has `field`, `before`, and `after`. Event-specific metadata (e.g. linked `renewal_cycle_id` or `order_id`). ```json Response example theme={null} { "subscription_log": { "id": "slog_123", "subscription_id": "sub_123", "event_type": "renewal.succeeded", "actor_type": "scheduler", "actor_id": null, "actor": { "type": "scheduler", "id": null, "email": null, "name": null, "display": null }, "subscription": { "subscription_id": "sub_123", "reference": "SUB-001", "customer_id": "cus_123", "customer_name": "Jane Doe", "product_title": "Coffee Subscription", "variant_title": "1 kg" }, "reason": null, "change_summary": "status, processed_at", "created_at": "2026-04-15T10:03:00.000Z", "previous_state": { "status": "scheduled" }, "new_state": { "status": "succeeded" }, "changed_fields": [ { "field": "status", "before": "scheduled", "after": "succeeded" } ], "metadata": { "renewal_cycle_id": "re_123", "order_id": "order_123" } } } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | ---------------------------------- | | `404` | `not_found` | Activity log record does not exist | *** ## GET /admin/subscriptions/:id/logs Returns the paginated activity log timeline scoped to a single subscription. Accepts the same query parameters as the global list and applies `subscription_id = :id` automatically. ### Path parameters Subscription ID. ### Query parameters Same as `GET /admin/subscription-logs`. Default sort is `created_at desc`. ### Response Same shape as `GET /admin/subscription-logs`. ```json Response example theme={null} { "subscription_logs": [ { "id": "slog_123", "subscription_id": "sub_123", "event_type": "subscription.paused", "actor_type": "user", "actor_id": "user_123", "actor": { "type": "user", "id": "user_123", "email": "admin@example.com", "name": "Admin User", "display": "admin@example.com" }, "subscription": { "subscription_id": "sub_123", "reference": "SUB-001", "customer_id": "cus_123", "customer_name": "Jane Doe", "product_title": "Coffee Subscription", "variant_title": "1 kg" }, "reason": "customer requested pause", "change_summary": "status, paused_at", "created_at": "2026-04-15T10:00:00.000Z" } ], "count": 1, "limit": 20, "offset": 0 } ``` # Analytics Source: https://docs.reorderjs.com/api/admin-analytics Query KPI summaries, time-series trends, and export analytics data — plus trigger manual snapshot rebuilds for historical date ranges. The subscription analytics endpoints power the Admin analytics dashboard. You can fetch KPI summary cards, retrieve grouped time-series data for charts, export a flattened dataset as JSON or CSV, and trigger a manual rebuild of daily analytics snapshots for a historical range. All routes require an authenticated Medusa Admin user. Read routes share a common filter contract; the rebuild route uses a separate request body. All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`. ## Metric keys | Key | Unit | Description | | ----------------------------- | ------------ | -------------------------------------------- | | `mrr` | `currency` | Monthly recurring revenue | | `churn_rate` | `percentage` | Subscriber churn rate | | `ltv` | `currency` | Estimated lifetime value | | `active_subscriptions_count` | `count` | Number of active subscriptions | | `created_subscriptions_count` | `count` | Number of subscriptions created on a UTC day | `MRR` and `LTV` resolve to `null` when the selected dataset lacks a single valid currency context. `created_subscriptions_count` is returned by the trends response only. KPI cards remain `mrr`, `churn_rate`, `ltv`, and `active_subscriptions_count`. ## Shared filter parameters All three read routes (`/kpis`, `/trends`, `/export`) accept the same filter parameters. ISO datetime lower bound. Must be on or before `date_to`. Maximum analytics window is 731 days. ISO datetime upper bound. Filter by subscription status. Supported values: `active`, `paused`, `cancelled`, `past_due`. Filter by product ID. Accepts a single value or an array. Filter by cadence using serialized tokens such as `week:1`, `month:1`, or `year:1`. Accepts a single value or an array. Time bucket granularity. One of `day`, `week`, or `month`. Defaults to `day`. Timezone for bucket boundaries. Only `UTC` is supported in MVP. *** ## GET /admin/subscription-analytics/kpis Returns the KPI summary payload used by the Admin analytics overview cards. ### Response Array of KPI summaries, one per supported metric key. Metric key. Human-readable label. Computed metric value, or `null` when not computable for the selected range. One of `currency`, `percentage`, or `count`. Currency code for currency metrics, or `null`. Decimal precision for display formatting. Value for the prior comparable period. Absolute change from previous period. Percentage change from previous period. Resolved filter values used for the query. Analytics schema version identifier. Timestamp when the response was generated. ```json Response example theme={null} { "filters": { "date_from": "2026-04-01T00:00:00.000Z", "date_to": "2026-04-30T23:59:59.999Z", "status": ["active", "past_due"], "product_id": ["prod_123"], "frequency": [ { "interval": "month", "value": 1 } ], "group_by": "day" }, "metrics_version": "analytics-v1", "generated_at": "2026-05-01T10:00:00.000Z", "kpis": [ { "key": "mrr", "label": "MRR", "value": 2480, "unit": "currency", "currency_code": "usd", "precision": 2, "previous_value": 2310, "delta_value": 170, "delta_percentage": 7.36 }, { "key": "churn_rate", "label": "Churn Rate", "value": 3.2, "unit": "percentage", "currency_code": null, "precision": 2, "previous_value": 4.1, "delta_value": -0.9, "delta_percentage": -21.95 }, { "key": "ltv", "label": "LTV", "value": 412, "unit": "currency", "currency_code": "usd", "precision": 2, "previous_value": 398, "delta_value": 14, "delta_percentage": 3.52 }, { "key": "active_subscriptions_count", "label": "Active Subscriptions", "value": 182, "unit": "count", "currency_code": null, "precision": 0, "previous_value": 176, "delta_value": 6, "delta_percentage": 3.41 } ] } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ---------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid filter shape, unsupported grouping value, or invalid frequency token | *** ## GET /admin/subscription-analytics/trends Returns grouped time-series data for Admin analytics charts. Each series covers one metric key and contains one point per time bucket within the requested range. `created_subscriptions_count` is a special-case series: * it is sourced from `subscription.created_at` * it always returns one UTC day bucket per point * it ignores `status`, `product_id`, `frequency`, and `group_by` * it zero-fills missing days inside the selected range ### Response Array of trend series, one per metric key. Metric key. Human-readable label. One of `currency`, `percentage`, or `count`. Currency code for currency metrics, or `null`. Decimal precision for display formatting. Ordered time-series points. Each point has `bucket_start`, `bucket_end`, and `value` (may be `null`). Resolved filter values. Analytics schema version identifier. Timestamp when the response was generated. ```json Response example theme={null} { "filters": { "date_from": "2026-04-01T00:00:00.000Z", "date_to": "2026-04-30T23:59:59.999Z", "status": ["active"], "product_id": [], "frequency": [], "group_by": "week" }, "metrics_version": "analytics-v1", "generated_at": "2026-05-01T10:00:00.000Z", "series": [ { "metric": "mrr", "label": "MRR", "unit": "currency", "currency_code": "usd", "precision": 2, "points": [ { "bucket_start": "2026-03-30T00:00:00.000Z", "bucket_end": "2026-04-05T23:59:59.999Z", "value": 2280 }, { "bucket_start": "2026-04-06T00:00:00.000Z", "bucket_end": "2026-04-12T23:59:59.999Z", "value": 2330 } ] }, { "metric": "active_subscriptions_count", "label": "Active Subscriptions", "unit": "count", "currency_code": null, "precision": 0, "points": [ { "bucket_start": "2026-03-30T00:00:00.000Z", "bucket_end": "2026-04-05T23:59:59.999Z", "value": 174 }, { "bucket_start": "2026-04-06T00:00:00.000Z", "bucket_end": "2026-04-12T23:59:59.999Z", "value": 178 } ] }, { "metric": "created_subscriptions_count", "label": "Created Subscriptions", "unit": "count", "currency_code": null, "precision": 0, "points": [ { "bucket_start": "2026-04-01T00:00:00.000Z", "bucket_end": "2026-04-01T23:59:59.999Z", "value": 3 }, { "bucket_start": "2026-04-02T00:00:00.000Z", "bucket_end": "2026-04-02T23:59:59.999Z", "value": 0 } ] } ] } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ---------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid filter shape, unsupported grouping value, or invalid frequency token | *** ## GET /admin/subscription-analytics/export Returns an export payload aligned with the active analytics filters. The export is synchronous and supports `json` and `csv` formats. Rows represent one entry per time bucket. ### Additional query parameter Export format. One of `json` or `csv`. Defaults to `json`. ### Response Resolved export format. Resolved filter values. Analytics schema version identifier. Timestamp when the export was generated. Suggested file name for the export. MIME type for the export format. Ordered column names for the export dataset. Flattened export rows. Each row contains one value per column. Metric cells may be `null`. ```json Response example theme={null} { "format": "json", "filters": { "date_from": "2026-04-01T00:00:00.000Z", "date_to": "2026-04-30T23:59:59.999Z", "status": ["active"], "product_id": [], "frequency": [], "group_by": "month" }, "metrics_version": "analytics-v1", "generated_at": "2026-05-01T10:00:00.000Z", "file_name": "subscription-analytics-2026-05-01.json", "content_type": "application/json", "columns": [ "bucket_start", "bucket_end", "mrr", "churn_rate", "ltv", "active_subscriptions_count" ], "rows": [ { "bucket_start": "2026-04-01T00:00:00.000Z", "bucket_end": "2026-04-30T23:59:59.999Z", "mrr": 2480, "churn_rate": 3.2, "ltv": 412, "active_subscriptions_count": 182 } ] } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid filter shape, unsupported grouping value, unsupported export format, or invalid frequency token | *** ## POST /admin/subscription-analytics/rebuild Triggers a manual rebuild of daily analytics snapshots for a historical date range. Reuses the same shared workflow used by scheduled and incremental analytics jobs. Rebuild is day-level idempotent — rerunning the same range is safe. ### Body parameters ISO datetime start of the rebuild range. ISO datetime end of the rebuild range. Maximum manual rebuild window is 365 days. Optional reason for the rebuild, recorded for audit purposes. ```json Request example theme={null} { "date_from": "2026-04-01T00:00:00.000Z", "date_to": "2026-04-30T23:59:59.999Z", "reason": "historical backfill after metrics review" } ``` ### Response Rebuild range start. Rebuild range end. Number of calendar days processed. Total subscription records evaluated. Snapshot rows created or updated. Rows skipped during the rebuild. Days that could not be processed. Days where processing failed. Partial failure does not change the HTTP status to `500`. ```json Response example theme={null} { "date_from": "2026-04-01T00:00:00.000Z", "date_to": "2026-04-30T23:59:59.999Z", "processed_days": 30, "processed_subscriptions": 1240, "upserted_rows": 1240, "skipped_rows": 0, "blocked_days": [], "failed_days": [] } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------------------- | | `400` | `invalid_data` | Invalid request body or rebuild window exceeds 365 days | # Cancellations Source: https://docs.reorderjs.com/api/admin-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. All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`. ## 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 Number of results per page. Zero-based result offset for pagination. Free-text search across subscription reference, customer name, and product title. 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`. Sort direction. One of `asc` or `desc`. Filter by case status. Accepts a single value or an array. Filter by final outcome. Accepts a single value or an array. Filter by churn reason category. Accepts a single value or an array. Filter by retention offer type. Accepts a single value or an array. Filter to cases for a specific subscription. ISO datetime lower bound for `created_at`. ISO datetime upper bound for `created_at`. ### Response Array of cancellation case list items. Cancellation case ID. Current case status. Free-text churn reason. Normalized reason category. Final outcome once the case is closed, or `null` while open. Subscription summary including reference, status, customer name, product info, and renewal timestamps. Case creation timestamp. When the case was closed, or `null` while open. Last update timestamp. Total matching records. Page size used. Result offset used. ```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 Cancellation case ID. ### Response Cancellation case ID. Current case status. Free-text churn reason. Normalized reason category. Final outcome once closed. Full subscription summary. Operator notes. Actor ID who finalized the case. When the cancellation takes effect. Linked dunning summary, or `null`. Linked renewal cycle summary, or `null`. Ordered list of retention offer events. Operational metadata. Creation timestamp. Finalization timestamp, or `null`. Last update timestamp. ```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 Cancellation case ID. ### Body parameters Type of retention offer. One of `pause_offer`, `discount_offer`, or `bonus_offer`. Offer-specific configuration. Shape depends on `offer_type` — see examples below. Actor ID of the admin user applying the offer. Optional reason for applying the offer. ```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" } ``` 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 | 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 Cancellation case ID. ### Body parameters Free-text churn reason. Normalized reason category. Operator notes. Actor ID of the admin user making the update. Reason for the classification change. ```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 Cancellation case ID. ### Body parameters Churn reason for the final cancellation. Required by domain rules; if omitted, the workflow falls back to the existing case reason. Normalized reason category. Operator notes. Actor ID of the admin user finalizing the case. When the cancellation takes effect. One of `immediately` or `end_of_cycle`. ```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 | # Dunning Source: https://docs.reorderjs.com/api/admin-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. All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`. ## 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 Number of results per page. Zero-based result offset for pagination. Free-text search across subscription reference, customer name, and product title. 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`. Sort direction. One of `asc` or `desc`. Filter by case status. Accepts a single value or an array. Filter to cases for a specific subscription. Filter by the originating renewal cycle ID. Filter by the renewal order ID. Filter by payment provider ID. Filter by the last payment error code (e.g. `card_declined`). Minimum number of attempts filter. Maximum number of attempts filter. ISO datetime lower bound for `next_retry_at`. ISO datetime upper bound for `next_retry_at`. Filter by the status of the most recent attempt. ### Response Array of dunning case list items. Dunning case ID. Current case status. Subscription summary including reference, status, customer name, product info, and payment provider ID. Linked renewal cycle summary with `renewal_cycle_id`, `status`, `scheduled_for`, and `generated_order_id`. Order summary with `order_id`, `display_id`, and `status`, or `null` if not yet generated. Number of retry attempts made so far. Maximum allowed retry attempts. Scheduled timestamp for the next automatic retry. Timestamp of the most recent attempt. Error code from the last payment attempt. Last update timestamp. Total matching records. Page size used. Result offset used. ```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 Dunning case ID. ### Response Returns a `dunning_case` object with all list fields plus: Active retry policy with `strategy`, `intervals` (array of minutes), `timezone`, and `source`. Human-readable error message from the last payment attempt. Timestamp when the case was marked recovered. Timestamp when the case was closed. Reason recorded when the case was closed. Full attempt history with attempt number, status, timestamps, error code, error message, payment reference, and metadata. Operational metadata such as `origin`. Creation timestamp. ```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 Dunning case ID. ### Body parameters Optional reason for the manual retry. ```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 Dunning case ID. ### Body parameters Optional reason for the manual recovery. ```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 Dunning case ID. ### Body parameters Reason for closing the case as unrecovered. Required. ```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 Dunning case ID. ### Body parameters Array of retry intervals in minutes. Each value must be a positive integer. Maximum number of retry attempts. Must be a positive integer equal to the length of `intervals`. Optional reason for the schedule override. ```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 | # Plans & Offers Source: https://docs.reorderjs.com/api/admin-plans-offers Create, update, toggle, and inspect subscription plan offers that control available frequencies, discounts, and rules for products or variants. Plan offer endpoints let you configure which subscription frequencies and discounts are available for each product or variant. You can list all offers, fetch a single offer's full detail including the effective configuration resolved from variant-to-product fallback semantics, create or upsert offers, update existing records, and toggle enabled state without touching other fields. 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`. ## Domain values | Domain | Supported values | | ------------------ | ------------------------------------------------------------ | | Status | `enabled`, `disabled` | | Scope | `product`, `variant` | | Frequency interval | `week`, `month`, `year` | | Discount type | `percentage`, `fixed` | | Stacking policy | `allowed`, `disallow_all`, `disallow_subscription_discounts` | *** ## GET /admin/subscription-offers Returns the paginated plan offer list for Admin DataTable views. ### Query parameters Number of results per page. Zero-based result offset for pagination. Free-text search across name and product/variant titles. Field to sort by. Database-backed: `name`, `scope`, `is_enabled`, `created_at`, `updated_at`. In-memory: `status`, `product_title`, `variant_title`. Sort direction. One of `asc` or `desc`. Filter by enabled/disabled state. Filter by target scope. One of `product` or `variant`. Filter by product ID. Filter by variant ID. Filter by frequency interval. One of `week`, `month`, or `year`. Minimum discount value filter. Maximum discount value filter. ### Response Array of plan offer list items. Plan offer ID. Offer name. Human-readable status label. Whether the offer is enabled. Scoped target with `scope`, `product_id`, `product_title`, and optionally `variant_id`, `variant_title`, `sku`. Array of allowed cadences with `interval`, `value`, and `label`. Array of per-frequency discount definitions. Human-readable rules summary. Resolved effective configuration including source scope, allowed frequencies, discounts, and rules. Last update timestamp. Total matching records. Page size used. Result offset used. ```json Response example theme={null} { "plan_offers": [ { "id": "po_123", "name": "Coffee Monthly Variant Offer", "status": "enabled", "is_enabled": true, "target": { "scope": "variant", "product_id": "prod_123", "product_title": "Coffee Subscription", "variant_id": "variant_123", "variant_title": "1 kg", "sku": "COFFEE-1KG" }, "allowed_frequencies": [ { "interval": "month", "value": 1, "label": "Every month" } ], "discounts": [ { "interval": "month", "frequency_value": 1, "type": "percentage", "value": 10, "label": "10% off" } ], "rules_summary": "Min 1 cycles · Stacking allowed", "effective_config_summary": { "source_scope": "variant", "source_offer_id": "po_123", "allowed_frequencies": [{ "interval": "month", "value": 1, "label": "Every month" }], "discounts": [{ "interval": "month", "frequency_value": 1, "type": "percentage", "value": 10, "label": "10% off" }], "rules": { "minimum_cycles": 1, "trial_enabled": false, "trial_days": null, "stacking_policy": "allowed" } }, "updated_at": "2026-03-29T12:00: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/subscription-offers/:id Returns the full detail payload for a single plan offer. ### Path parameters Plan offer ID. ### Response Returns a `plan_offer` object with all list fields plus: Full rules object with `minimum_cycles`, `trial_enabled`, `trial_days`, and `stacking_policy`. Arbitrary metadata. Creation timestamp. ```json Response example theme={null} { "plan_offer": { "id": "po_123", "name": "Coffee Monthly Variant Offer", "status": "enabled", "is_enabled": true, "target": { "scope": "variant", "product_id": "prod_123", "product_title": "Coffee Subscription", "variant_id": "variant_123", "variant_title": "1 kg", "sku": "COFFEE-1KG" }, "allowed_frequencies": [{ "interval": "month", "value": 1, "label": "Every month" }], "discounts": [ { "interval": "month", "frequency_value": 1, "type": "percentage", "value": 10, "label": "10% off" } ], "rules_summary": "Min 1 cycles · Stacking allowed", "effective_config_summary": { "source_scope": "variant", "source_offer_id": "po_123", "allowed_frequencies": [{ "interval": "month", "value": 1, "label": "Every month" }], "discounts": [{ "interval": "month", "frequency_value": 1, "type": "percentage", "value": 10, "label": "10% off" }], "rules": { "minimum_cycles": 1, "trial_enabled": false, "trial_days": null, "stacking_policy": "allowed" } }, "created_at": "2026-03-29T10:00:00.000Z", "updated_at": "2026-03-29T12:00:00.000Z", "rules": { "minimum_cycles": 1, "trial_enabled": false, "trial_days": null, "stacking_policy": "allowed" }, "metadata": { "source": "admin" } } } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | ------------------------- | | `404` | `not_found` | Plan offer does not exist | *** ## POST /admin/subscription-offers Creates a new plan offer, or updates an existing one for the same target (create-or-upsert semantics). ### Body parameters Offer name. Trimmed. Target scope. One of `product` or `variant`. Target product ID. Target variant ID. Required for `variant`-scoped offers; must be omitted or `null` for `product`-scoped offers. Whether the offer is active. Array of allowed cadences. Each entry requires `interval` (`week`, `month`, or `year`) and a positive integer `value`. Must contain at least one entry. Per-frequency discount definitions. Each entry requires `interval`, `frequency_value`, `type` (`percentage` or `fixed`), and `value`. Optional. Rules object with `minimum_cycles`, `trial_enabled`, `trial_days`, and `stacking_policy`. Optional. Arbitrary metadata. Optional. ```json Request example theme={null} { "name": "Coffee Monthly Variant Offer", "scope": "variant", "product_id": "prod_123", "variant_id": "variant_123", "is_enabled": true, "allowed_frequencies": [{ "interval": "month", "value": 1 }], "discounts": [{ "interval": "month", "frequency_value": 1, "type": "percentage", "value": 10 }], "rules": { "minimum_cycles": 1, "trial_enabled": false, "trial_days": null, "stacking_policy": "allowed" }, "metadata": { "source": "admin" } } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | --------------------------------------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid request shape | | `400` | `invalid_data` | Product-scoped offer specifies `variant_id`, or variant-scoped offer omits it | | `400` | `invalid_data` | Product does not exist, or variant does not belong to the product | | `400` | `invalid_data` | Duplicate or invalid frequency definitions | | `400` | `invalid_data` | Discount defined for a frequency not in `allowed_frequencies`, invalid discount range, or invalid trial configuration | | `409` | `conflict` | Conflicting override configuration from an inconsistent persisted state | *** ## POST /admin/subscription-offers/:id Updates an existing plan offer source record. At least one field must be provided. ### Path parameters Plan offer ID. ### Body parameters All fields are optional; omit any field you do not want to change. Updated offer name. Updated enabled state. Replacement list of allowed cadences. Replacement list of per-frequency discounts. Updated rules object. Updated metadata. ```json Request example theme={null} { "name": "Coffee Monthly Variant Offer Updated", "is_enabled": true, "allowed_frequencies": [ { "interval": "month", "value": 2 }, { "interval": "year", "value": 1 } ], "discounts": [ { "interval": "month", "frequency_value": 2, "type": "percentage", "value": 12 } ], "rules": { "minimum_cycles": 2, "trial_enabled": true, "trial_days": 14, "stacking_policy": "disallow_subscription_discounts" }, "metadata": { "revision": 2 } } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | --------------------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Empty body with no fields, invalid request shape, or invalid frequency/discount/rules configuration | | `404` | `not_found` | Plan offer does not exist | *** ## POST /admin/subscription-offers/:id/toggle Enables or disables a plan offer without modifying any other fields. ### Path parameters Plan offer ID. ### Body parameters Target enabled state. ```json Request example theme={null} { "is_enabled": false } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------- | | `400` | `invalid_data` | Invalid request shape | | `404` | `not_found` | Plan offer does not exist | # Renewals Source: https://docs.reorderjs.com/api/admin-renewals List and inspect renewal cycles, approve or reject pending plan changes, and force-run cycles using the Reorder Admin renewals API endpoints. The renewal cycle endpoints expose the Admin renewal queue and let you act on individual cycles. You can list scheduled or failed cycles for DataTable views, fetch a full detail payload including attempt history and approval state, force-run a cycle manually, and approve or reject pending subscription changes before a cycle executes. 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 | Value | Meaning | | ------------ | ---------------------------------------------------- | | `scheduled` | Cycle is queued for its scheduled date | | `processing` | Cycle execution is in progress | | `succeeded` | Cycle completed successfully | | `failed` | Cycle failed; attempt history is available in detail | **Approval status values:** `pending`, `approved`, `rejected`. When approval is not required, the approval object returns `status: null`. **Attempt status values:** `processing`, `succeeded`, `failed`. *** ## GET /admin/renewals Returns the paginated renewal queue for Admin DataTable views. ### Query parameters Number of results per page. Zero-based result offset for pagination. Free-text search across subscription reference, customer name, and product title. Field to sort by. Database-backed: `scheduled_for`, `updated_at`, `created_at`, `status`, `approval_status`, `processed_at`. In-memory: `last_attempt_status`, `subscription_reference`, `customer_name`, `product_title`, `order_display_id`. Sort direction. One of `asc` or `desc`. Filter by cycle status. Filter by approval status. ISO datetime lower bound for `scheduled_for`. ISO datetime upper bound for `scheduled_for`. Filter by the status of the most recent attempt. Filter to renewals for a specific subscription. Filter by the ID of the order generated by a renewal. ### Response Array of renewal cycle list items. Renewal cycle ID. Cycle status. Subscription summary including reference, status, customer name, and product info. Operational renewal date stored on the cycle. Projected delivery date when the linked subscription has a skipped next cycle. Status of the most recent attempt. Timestamp of the most recent attempt. Approval summary with `required`, `status`, `decided_at`, `decided_by`, and `reason`. Generated order summary, or `null` if no order has been created. Last update timestamp. Total matching records. Page size used. Result offset used. ```json Response example theme={null} { "renewals": [ { "id": "re_123", "status": "scheduled", "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" }, "scheduled_for": "2026-04-15T10:00:00.000Z", "effective_scheduled_for": "2026-04-15T10:00:00.000Z", "last_attempt_status": "failed", "last_attempt_at": "2026-04-15T10:02:00.000Z", "approval": { "required": true, "status": "pending", "decided_at": null, "decided_by": null, "reason": null }, "generated_order": null, "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/renewals/:id Returns the full detail payload for a single renewal cycle, including the full attempt history and operational metadata. ### Path parameters Renewal cycle ID. ### Response Returns a `renewal` object with all list fields plus: Creation timestamp. When the cycle finished processing. Last error message, if any. Subscription plan changes pending approval for this cycle. Full attempt history with attempt number, status, timestamps, error code, error message, payment reference, and order ID. Operational metadata including `last_trigger_type` and `last_correlation_id`. ```json Response example theme={null} { "renewal": { "id": "re_123", "status": "failed", "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" }, "scheduled_for": "2026-04-15T10:00:00.000Z", "effective_scheduled_for": "2026-04-15T10:00:00.000Z", "last_attempt_status": "failed", "last_attempt_at": "2026-04-15T10:02:00.000Z", "approval": { "required": true, "status": "approved", "decided_at": "2026-04-15T09:55:00.000Z", "decided_by": "user_123", "reason": "approved for processing" }, "generated_order": { "order_id": "order_123", "display_id": 1001, "status": "pending" }, "updated_at": "2026-04-15T10:03:00.000Z", "created_at": "2026-04-10T10:00:00.000Z", "processed_at": "2026-04-15T10:03:00.000Z", "last_error": null, "pending_changes": { "variant_id": "variant_456", "variant_title": "2 kg", "frequency_interval": "month", "frequency_value": 2, "effective_at": null }, "attempts": [ { "id": "reatt_123", "attempt_no": 1, "status": "failed", "started_at": "2026-04-15T10:00:00.000Z", "finished_at": "2026-04-15T10:02:00.000Z", "error_code": "renewal_failed", "error_message": "payment failed", "payment_reference": null, "order_id": null } ], "metadata": { "last_trigger_type": "manual", "last_correlation_id": "renewal-admin-force-uuid" } } } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | ---------------------------- | | `404` | `not_found` | Renewal cycle does not exist | *** ## POST /admin/renewals/:id/force Manually triggers execution for a renewal cycle, ignoring its scheduled date. ### Path parameters Renewal cycle ID. ### Body parameters Optional reason for the manual trigger. ```json Request example theme={null} { "reason": "manual retry after review" } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------------------- | | `404` | `not_found` | Renewal cycle does not exist | | `409` | `conflict` | Cycle is already processing | | `409` | `conflict` | Cycle already succeeded; duplicate execution is blocked | | `409` | `conflict` | Cycle is not in a forceable state | | `409` | `conflict` | Cycle requires approved changes before force-run | | `409` | `conflict` | Linked subscription is not eligible for renewal | | `400` | `invalid_data` | Current Plans & Offers policy blocks the pending change | *** ## POST /admin/renewals/:id/approve-changes Approves pending subscription changes for a renewal cycle that requires approval before execution. ### Path parameters Renewal cycle ID. ### Body parameters Optional approval reason. ```json Request example theme={null} { "reason": "approved after review" } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | ------------------------------------------- | | `404` | `not_found` | Renewal cycle does not exist | | `409` | `conflict` | Approval is not required for this cycle | | `409` | `conflict` | Approval was already decided for this cycle | *** ## POST /admin/renewals/:id/reject-changes Rejects pending subscription changes for a renewal cycle. `reason` is required for rejections. ### Path parameters Renewal cycle ID. ### Body parameters Reason for rejection. Required. ```json Request example theme={null} { "reason": "pending changes are not valid for this cycle" } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------- | | `400` | `invalid_data` | Missing or invalid `reason` | | `404` | `not_found` | Renewal cycle does not exist | | `409` | `conflict` | Approval is not required for this cycle | | `409` | `conflict` | Approval was already decided for this cycle | # Settings Source: https://docs.reorderjs.com/api/admin-settings Read and update the global subscription settings singleton — dunning schedules, renewal behavior, cancellation policy, and trial defaults. The subscription settings endpoints manage the global configuration singleton used by the Reorder plugin at runtime. You can read the effective settings — which fall back to hardcoded defaults if no record has been persisted yet — and write a new or updated singleton through the dedicated workflow. All routes require an authenticated Medusa Admin user. The POST route uses optimistic locking via `expected_version`. All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`. ## Field reference | Field | Type | Description | | ------------------------------- | ---------- | ------------------------------------------------------------------------------------- | | `settings_key` | `string` | Always `global` in MVP. | | `default_trial_days` | `number` | Default trial length in days. Must be `>= 0`. | | `dunning_retry_intervals` | `number[]` | Retry schedule in minutes. Must be strictly increasing positive integers. | | `max_dunning_attempts` | `number` | Maximum retry attempts. Must equal the number of intervals. | | `default_renewal_behavior` | `string` | `process_immediately` or `require_review_for_pending_changes`. | | `default_cancellation_behavior` | `string` | `recommend_retention_first` or `allow_direct_cancellation`. | | `version` | `number` | Monotonic version for optimistic locking. | | `is_persisted` | `boolean` | `false` when the response uses fallback defaults; `true` when a stored record exists. | *** ## GET /admin/subscription-settings Returns the effective settings payload. When no record has been saved yet the response uses hardcoded fallback defaults and `is_persisted` is `false`. This route never returns `404`. ### Response Always `global`. Trial length in days. Retry intervals in minutes. Maximum retry attempts. Global renewal policy. Global cancellation policy. Current version number. Actor ID of the last update, or `null` when using defaults. Timestamp of the last update, or `null` when using defaults. Audit metadata including `audit_log` and `last_update`. `false` when returning fallback defaults. ```json Fallback response example theme={null} { "subscription_settings": { "settings_key": "global", "default_trial_days": 0, "dunning_retry_intervals": [1440, 4320, 10080], "max_dunning_attempts": 3, "default_renewal_behavior": "process_immediately", "default_cancellation_behavior": "recommend_retention_first", "version": 0, "updated_by": null, "updated_at": null, "metadata": null, "is_persisted": false } } ``` ```json Persisted response example theme={null} { "subscription_settings": { "settings_key": "global", "default_trial_days": 21, "dunning_retry_intervals": [45, 180, 720], "max_dunning_attempts": 3, "default_renewal_behavior": "require_review_for_pending_changes", "default_cancellation_behavior": "allow_direct_cancellation", "version": 1, "updated_by": "user_01ABC", "updated_at": "2026-04-03T14:00:00.000Z", "metadata": { "audit_log": [], "last_update": null }, "is_persisted": true } } ``` *** ## POST /admin/subscription-settings Persists a new or updated settings singleton through the dedicated workflow. The first successful POST creates the record. Omitted fields retain their current effective value. ### Body parameters Default trial length in days. Must be `>= 0`. Retry intervals in minutes. Must be strictly increasing positive integers. Length must equal `max_dunning_attempts`. Maximum retry attempts. Must be a positive integer equal to the number of intervals. Global renewal policy. One of `process_immediately` or `require_review_for_pending_changes`. Global cancellation policy. One of `recommend_retention_first` or `allow_direct_cancellation`. Current version for optimistic locking. Use `0` on the first write. Optional reason for the update, recorded in the audit log. ```json Request example theme={null} { "default_trial_days": 21, "dunning_retry_intervals": [45, 180, 720], "max_dunning_attempts": 3, "default_renewal_behavior": "require_review_for_pending_changes", "default_cancellation_behavior": "allow_direct_cancellation", "expected_version": 0, "reason": "admin_save" } ``` ### Response Returns the updated `subscription_settings` object. The `metadata.audit_log` and `metadata.last_update` fields record the change summary. ```json Response example theme={null} { "subscription_settings": { "settings_key": "global", "default_trial_days": 21, "dunning_retry_intervals": [45, 180, 720], "max_dunning_attempts": 3, "default_renewal_behavior": "require_review_for_pending_changes", "default_cancellation_behavior": "allow_direct_cancellation", "version": 1, "updated_by": "user_01ABC", "updated_at": "2026-04-03T14:00:00.000Z", "metadata": { "audit_log": [ { "action": "update_settings", "who": "user_01ABC", "when": "2026-04-03T14:00:00.000Z", "reason": "admin_save", "previous_version": 0, "next_version": 1, "change_summary": [ { "field": "default_trial_days", "from": 0, "to": 21 } ] } ], "last_update": { "action": "update_settings", "who": "user_01ABC", "when": "2026-04-03T14:00:00.000Z", "reason": "admin_save", "previous_version": 0, "next_version": 1, "change_summary": [ { "field": "default_trial_days", "from": 0, "to": 21 } ] } }, "is_persisted": true } } ``` ### Errors | Code | Error | Meaning | | ----- | ------------------ | ------------------------------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid scalar ranges, invalid enum values, invalid retry interval list, or mismatched `max_dunning_attempts` | | `409` | `conflict` | Stale `expected_version` — optimistic locking mismatch | | `500` | `unexpected_state` | Unexpected workflow or persistence failure | # Subscriptions Source: https://docs.reorderjs.com/api/admin-subscriptions Manage subscription lifecycle from the Medusa Admin: list, inspect, pause, resume, cancel, schedule plan changes, update shipping addresses, and manage payment methods. The subscriptions endpoints give you full control over individual subscription records. You can query the paginated list for DataTable views, fetch a full detail payload for a specific subscription, and trigger lifecycle mutations — pause, resume, cancel, schedule a future plan change, update the shipping address snapshot, or change the renewal payment method. All routes require an authenticated Medusa Admin user. All routes require an authenticated Medusa Admin user. Unauthenticated requests return `401`. ## Status values | Value | Meaning | | ----------- | ------------------------------------------ | | `active` | Subscription is billing normally | | `paused` | Subscription is on hold | | `cancelled` | Subscription has been terminated | | `past_due` | Last renewal failed; dunning may be active | *** ## GET /admin/subscriptions Returns the paginated subscription list for Admin DataTable views. ### Query parameters Number of results per page. Defaults to 20. Zero-based result offset for pagination. Free-text search across customer name, email, and reference. Field to sort by. Supported database-backed fields: `created_at`, `updated_at`, `status`, `frequency_interval`, `frequency_value`, `next_renewal_at`, `trial_ends_at`, `skip_next_cycle`. Supported in-memory fields: `customer_name`, `customer_email`, `product_title`, `variant_title`, `discount_value`. Sort direction. One of `asc` or `desc`. Filter by subscription status. Accepts a single value or an array. Filter by customer ID. Filter by product ID. Filter by variant ID. ISO datetime lower bound for `next_renewal_at`. ISO datetime upper bound for `next_renewal_at`. Filter to trial subscriptions only. Filter by whether the next cycle is skipped. ### Response Array of subscription list items. Subscription ID. Human-readable reference (e.g. `SUB-001`). Current subscription status. Customer summary including `id`, `full_name`, and `email`. Product summary including `product_id`, `product_title`, `variant_id`, `variant_title`, and `sku`. Cadence with `interval`, `value`, and `label`. Technical billing anchor date. Projected next renewal shown in Admin when `skip_next_cycle` is enabled. Trial info with `is_trial` and `trial_ends_at`. Applied discount with `type`, `value`, and `label`. Whether the next billing cycle is skipped. Last update timestamp. Total matching records. Page size used. Result offset used. ```json Response example theme={null} { "subscriptions": [ { "id": "sub_123", "reference": "SUB-001", "status": "active", "customer": { "id": "cus_123", "full_name": "Jane Doe", "email": "jane@example.com" }, "product": { "product_id": "prod_123", "product_title": "Coffee Subscription", "variant_id": "variant_123", "variant_title": "1 kg", "sku": "COFFEE-1KG" }, "frequency": { "interval": "month", "value": 1, "label": "Every month" }, "next_renewal_at": "2026-04-15T10:00:00.000Z", "effective_next_renewal_at": "2026-04-15T10:00:00.000Z", "trial": { "is_trial": false, "trial_ends_at": null }, "discount": { "type": "percentage", "value": 10, "label": "10% off" }, "skip_next_cycle": false, "updated_at": "2026-03-28T12:00: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/subscriptions/:id Returns the full detail payload for a single subscription. ### Path parameters Subscription ID. ### Response Returns a `subscription` object with all list fields plus: Creation timestamp. When the subscription became active. When the subscription was paused, or `null`. When the subscription was cancelled, or `null`. Last successful renewal timestamp. Full shipping address snapshot. Scheduled plan change data, or `null` when none is pending. Configured payment provider identifier, or `null`. Resolved summary of the current saved payment method, or `null`. ```json Response example theme={null} { "subscription": { "id": "sub_123", "reference": "SUB-001", "status": "active", "customer": { "id": "cus_123", "full_name": "Jane Doe", "email": "jane@example.com" }, "product": { "product_id": "prod_123", "product_title": "Coffee Subscription", "variant_id": "variant_123", "variant_title": "1 kg", "sku": "COFFEE-1KG" }, "frequency": { "interval": "month", "value": 1, "label": "Every month" }, "next_renewal_at": "2026-04-15T10:00:00.000Z", "effective_next_renewal_at": "2026-04-15T10:00:00.000Z", "trial": { "is_trial": false, "trial_ends_at": null }, "discount": { "type": "percentage", "value": 10, "label": "10% off" }, "skip_next_cycle": false, "updated_at": "2026-03-28T12:00:00.000Z", "created_at": "2026-03-01T10:00:00.000Z", "started_at": "2026-03-01T10:00:00.000Z", "paused_at": null, "cancelled_at": null, "last_renewal_at": "2026-03-15T10:00:00.000Z", "shipping_address": { "first_name": "Jane", "last_name": "Doe", "company": null, "address_1": "Main Street 1", "address_2": null, "city": "Warsaw", "postal_code": "00-001", "province": "Mazowieckie", "country_code": "PL", "phone": "+48123123123" }, "pending_update_data": { "variant_id": "variant_456", "variant_title": "2 kg", "frequency_interval": "month", "frequency_value": 2, "effective_at": "2026-05-01T00:00:00.000Z" }, "payment_provider_id": "pp_stripe_stripe", "payment_method": { "id": "pm_123", "provider_id": "pp_stripe_stripe", "type": "card", "brand": "visa", "last4": "4242", "exp_month": 4, "exp_year": 2030, "created_at": 1700000000, "is_current": true } } } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | --------------------------- | | `404` | `not_found` | Subscription does not exist | *** ## GET /admin/orders/:id/subscription-summary Returns the lightweight subscription context used by the custom `Subscription` widget on the native Medusa order detail page. ### Path parameters Medusa order ID. ### Response Whether the order is linked to a subscription. Subscription widget payload, or `null` for one-time orders. When linked, `summary.subscription` includes: * `id` * `reference` * `status` * `frequency_label` * `discount` * `next_renewal_at` * `effective_next_renewal_at` ```json Response example theme={null} { "summary": { "is_subscription_order": true, "subscription": { "id": "sub_123", "reference": "SUB-001", "status": "active", "frequency_label": "Every 2 weeks", "discount": { "type": "percentage", "value": 5, "label": "5% off" }, "next_renewal_at": "2026-05-07T10:00:00.000Z", "effective_next_renewal_at": "2026-05-07T10:00:00.000Z" } } } ``` The `discount` field is derived from the subscription's `pricing_snapshot`, not from Medusa's built-in order summary rows. *** ## POST /admin/subscriptions/:id/pause Pauses an active subscription. All mutation routes return the refreshed full detail payload. ### Path parameters Subscription ID. ### Body parameters Optional reason for the pause. ISO datetime for when the pause takes effect. Optional. ```json Request example theme={null} { "reason": "customer requested temporary stop", "effective_at": "2026-04-01T00:00:00.000Z" } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ---------------------------------------------------- | | `400` | `invalid_data` | Invalid body payload | | `404` | `not_found` | Subscription does not exist | | `409` | `conflict` | Subscription cannot be paused from its current state | *** ## POST /admin/subscriptions/:id/resume Resumes a paused subscription. ### Path parameters Subscription ID. ### Body parameters ISO datetime for when the subscription resumes. Optional. When `true`, keeps the original billing anchor date. Optional. ```json Request example theme={null} { "resume_at": "2026-04-15T00:00:00.000Z", "preserve_billing_anchor": true } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ----------------------------------------------------- | | `400` | `invalid_data` | Invalid body payload | | `404` | `not_found` | Subscription does not exist | | `409` | `conflict` | Subscription cannot be resumed from its current state | *** ## POST /admin/subscriptions/:id/cancel Cancels a subscription immediately or at end of cycle. ### Path parameters Subscription ID. ### Body parameters Optional cancellation reason. When to apply the cancellation. One of `immediately` or `end_of_cycle`. Optional. ```json Request example theme={null} { "reason": "retention flow failed", "effective_at": "end_of_cycle" } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------------------- | | `400` | `invalid_data` | Invalid body payload | | `404` | `not_found` | Subscription does not exist | | `409` | `conflict` | Subscription cannot be cancelled from its current state | *** ## POST /admin/subscriptions/:id/schedule-plan-change Stores a future plan or cadence update in `pending_update_data`. The change is applied at the next renewal or at the specified `effective_at` date. ### Path parameters Subscription ID. ### Body parameters Target variant ID for the plan change. New billing interval. One of `week`, `month`, or `year`. New billing cadence as a positive integer. ISO datetime when the change becomes effective. Optional. ```json Request example theme={null} { "variant_id": "variant_456", "frequency_interval": "month", "frequency_value": 2, "effective_at": "2026-05-01T00:00:00.000Z" } ``` `pending_update_data` is returned inside the refreshed subscription detail payload after the change is scheduled. ### Errors | Code | Error | Meaning | | ----- | -------------- | ------------------------------------------------------------- | | `400` | `invalid_data` | Invalid body payload | | `404` | `not_found` | Subscription does not exist | | `409` | `conflict` | Plan change is not allowed for the current subscription state | *** ## POST /admin/subscriptions/:id/update-shipping-address Updates the subscription shipping address snapshot used by Admin and future operational flows. ### Path parameters Subscription ID. ### Body parameters First name. Last name. Primary address line. City. Postal or ZIP code. Two-letter ISO country code. Company name. Optional. Secondary address line. Optional. Province or state. Optional. Phone number. Optional. ```json Request example theme={null} { "first_name": "Anna", "last_name": "Nowak", "company": "ACME", "address_1": "Nowa 2", "address_2": "lok. 4", "city": "Krakow", "postal_code": "30-001", "province": "Malopolskie", "country_code": "PL", "phone": "+48111111111" } ``` ### Errors | Code | Error | Meaning | | ----- | -------------- | --------------------------- | | `400` | `invalid_data` | Invalid body payload | | `404` | `not_found` | Subscription does not exist | *** ## GET /admin/subscriptions/:id/payment-methods Lists the saved payment methods of the subscription's customer for the subscription's payment provider. ### Path parameters Subscription ID. ### Response The payment provider ID associated with the subscription, or `null` if none is configured. Payment method identifier. Payment provider identifier. Type of payment method (e.g. `card`). Card brand (e.g. `visa`, `mastercard`). Last 4 digits of the card number. Card expiration month. Card expiration year. Whether this is the payment method currently assigned to the subscription. ```json Response example theme={null} { "payment_provider_id": "pp_stripe_stripe", "payment_methods": [ { "id": "pm_123", "provider_id": "pp_stripe_stripe", "type": "card", "brand": "visa", "last4": "4242", "exp_month": 4, "exp_year": 2030, "created_at": 1700000000, "is_current": true } ] } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | --------------------------- | | `404` | `not_found` | Subscription does not exist | *** ## POST /admin/subscriptions/:id/payment-method Changes the payment method the subscription renews with. ### Path parameters Subscription ID. ### Body parameters Identifier of the saved payment method. Payment provider identifier. Optional if the subscription already has a configured provider. ```json Request example theme={null} { "payment_method_id": "pm_123", "provider_id": "pp_stripe_stripe" } ``` ### Response Returns the full subscription detail payload (`SubscriptionAdminDetailResponse`). ### Errors | Code | Error | Meaning | | ----- | -------------- | ----------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid body payload, unknown payment method, or payment method not owned by the customer | | `404` | `not_found` | Subscription does not exist | | `409` | `conflict` | Subscription status does not allow updating the payment method | # Subscription Checkout Source: https://docs.reorderjs.com/api/store-checkout Convert a prepared cart into a subscription order using line item metadata as the source of truth for billing cadence. Converts a cart to a subscription order at checkout. The cart must contain exactly one subscription line item with quantity `1`. Line item metadata is the authoritative source for subscription configuration. Mixed carts (subscription and one-time items together) are not supported in the current version. Standard one-time checkout is not affected by this route. This endpoint is idempotent — if the completed order is already linked to a subscription, the existing subscription is returned instead of creating a duplicate. *** ## POST /store/carts/:id/subscribe ### Path parameters Cart ID. ### Line item metadata contract The following metadata fields must be set on the subscription line item before calling this endpoint. Must be `true` to mark the line item as a subscription. Billing interval. One of `week`, `month`, or `year`. Billing cadence as a positive integer. ### Optional cart metadata When present, must be `"subscription"`. Mixed purchase mode is not supported. ```json Line item metadata example theme={null} { "metadata": { "is_subscription": true, "frequency_interval": "month", "frequency_value": 1 } } ``` ### Response Returns the created subscription on success. If the order is already linked to a subscription (idempotent re-call), returns the existing subscription. When the subscription is created for the first time, Reorder also appends a `subscription.created` entry to the Activity Log. The event is recorded with the storefront customer as the actor so operators can see that the subscription originated from checkout. ### Errors | Code | Error | Meaning | | ----- | -------------- | -------------------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Mixed cart, missing subscription line item, invalid metadata, or unsupported `purchase_mode` value | # Subscription Offers Source: https://docs.reorderjs.com/api/store-offers Resolve the effective subscription offer for a product or variant — used to power PDP frequency selectors, savings displays, and checkout validation. Returns the effective subscription offer for a product, optionally scoped to a specific variant. Variant-level offers take precedence over product-level offers. Returns `is_subscription_available: false` when no active offer exists. *** ## GET /store/products/:id/subscription-offer ### Path parameters Product ID. ### Query parameters Optional variant ID. When provided, the response reflects the variant-level offer if one exists. ### Response Whether a subscription offer is available for this product or variant. Resolved product ID. Resolved variant ID, or `null` when the offer applies at product level. ID of the Plans & Offers record that sourced this offer. Scope of the source offer: `product` or `variant`. Cadences the customer can choose. Each item contains `frequency_interval`, `frequency_value`, `label`, and a `discount` object with `type` and `value`. Global discount presentation rules for the offer, or `null`. Minimum number of billing cycles required, or `null`. Trial configuration with duration and terms, or `null` when no trial applies. ```json Response example — offer available theme={null} { "subscription_offer": { "is_subscription_available": true, "product_id": "prod_123", "variant_id": "variant_123", "source_offer_id": "offer_456", "source_scope": "variant", "allowed_frequencies": [ { "frequency_interval": "month", "frequency_value": 1, "label": "Every month", "discount": { "type": "percentage", "value": 10 } }, { "frequency_interval": "month", "frequency_value": 2, "label": "Every 2 months", "discount": { "type": "percentage", "value": 5 } } ], "discount_semantics": null, "minimum_cycles": null, "trial": null } } ``` ```json Response example — no offer theme={null} { "subscription_offer": { "is_subscription_available": false, "product_id": "prod_789", "variant_id": null, "source_offer_id": null, "source_scope": null, "allowed_frequencies": [], "discount_semantics": null, "minimum_cycles": null, "trial": null } } ``` # Customer Subscriptions Source: https://docs.reorderjs.com/api/store-subscriptions Let customers list and manage their own subscriptions — pause, resume, skip, swap, change frequency, address, or payment method, retry payment, and request cancellation. The customer subscription endpoints expose self-service subscription management on the storefront. An authenticated customer can view their subscriptions, make lifecycle changes, manage saved payment methods, and initiate a cancellation. All routes require a valid customer session or bearer token, and all subscription mutations enforce ownership — the subscription must belong to the authenticated customer. Mutations return the refreshed subscription detail payload unless noted otherwise. All routes use `authenticate("customer", ["session", "bearer"])`. Ownership is validated against the authenticated customer's `actor_id`. *** ## GET /store/customers/me/subscriptions Returns the authenticated customer's subscriptions with storefront summary data. ### Response Subscription ID. Human-readable reference (e.g. `SUB-001`). Current subscription status. Product name. Variant name. Next scheduled renewal date. Active cancellation case summary, or `null`. *** ## GET /store/customers/me/subscriptions/:id Returns the full storefront-safe detail payload for one subscription. ### Path parameters Subscription ID. Must belong to the authenticated customer. ### Response Subscription ID. Human-readable reference. Current subscription status. Product name. Variant name. Billing interval: `week`, `month`, or `year`. Billing cadence as a positive integer. Technical billing anchor date. Projected next renewal accounting for any skip. Last successful renewal timestamp. Current shipping address snapshot. Current payment status. Payment provider identifier. Summary of the current saved payment method (brand, last4, expiry), or `null`. Active dunning recovery case, or `null`. Pending variant or cadence change, or `null`. Active cancellation case summary, or `null`. *** ## POST /store/customers/me/subscriptions/:id/pause Pauses the customer's subscription. ### Path parameters Subscription ID. ### Body parameters Optional reason for the pause. ISO datetime for when the pause takes effect. Optional. ```json Request example theme={null} { "reason": "Taking a short break", "effective_at": "2026-04-15T10:00:00.000Z" } ``` *** ## POST /store/customers/me/subscriptions/:id/resume Resumes a paused subscription. ### Path parameters Subscription ID. ### Body parameters ISO datetime for when the subscription resumes. Optional. When `true`, keeps the original billing anchor date. Optional. ```json Request example theme={null} { "resume_at": "2026-04-20T10:00:00.000Z", "preserve_billing_anchor": true } ``` *** ## POST /store/customers/me/subscriptions/:id/change-frequency Schedules a cadence change for the subscription. The current variant stays unchanged. The new cadence is validated against active Plans & Offers. ### Path parameters Subscription ID. ### Body parameters New billing interval. One of `week`, `month`, or `year`. New billing cadence as a positive integer. ISO datetime when the change becomes effective. Optional. ```json Request example theme={null} { "frequency_interval": "month", "frequency_value": 2, "effective_at": "2026-05-01T10:00:00.000Z" } ``` *** ## POST /store/customers/me/subscriptions/:id/change-address Updates the subscription shipping address. ### Path parameters Subscription ID. ### Body parameters First name. Last name. Primary address line. City. Postal or ZIP code. Two-letter ISO country code. ```json Request example theme={null} { "first_name": "Jane", "last_name": "Doe", "address_1": "Main Street 1", "city": "Copenhagen", "postal_code": "2100", "country_code": "dk" } ``` *** ## POST /store/customers/me/subscriptions/:id/skip-next-delivery Marks the next renewal cycle as skipped. No request body is required. ### Path parameters Subscription ID. *** ## POST /store/customers/me/subscriptions/:id/swap-product Schedules a product or variant swap. The target variant must belong to the subscription product and be allowed by active Plans & Offers. ### Path parameters Subscription ID. ### Body parameters Target variant ID. Billing interval for the new variant. One of `week`, `month`, or `year`. Billing cadence as a positive integer. ISO datetime when the swap becomes effective. Optional. ```json Request example theme={null} { "variant_id": "variant_123", "frequency_interval": "month", "frequency_value": 1, "effective_at": "2026-05-01T10:00:00.000Z" } ``` *** ## POST /store/customers/me/subscriptions/:id/retry-payment Runs a manual payment retry for a retry-eligible dunning recovery case. ### Path parameters Subscription ID. ### Body parameters Optional reason for the manual retry. ```json Request example theme={null} { "reason": "Customer requested immediate retry" } ``` ### Errors | Code | Error | Meaning | | ----- | ---------- | ---------------------------------------------- | | `409` | `conflict` | No retry-eligible payment recovery case exists | *** ## POST /store/customers/me/subscriptions/:id/cancellation Starts a cancellation case for the subscription. Returns a minimal cancellation case payload rather than the full subscription detail. ### Path parameters Subscription ID. Must belong to the authenticated customer. ### Body parameters Free-text churn reason. Normalized reason category. One of `price`, `product_fit`, `delivery`, `billing`, `temporary_pause`, `switched_competitor`, or `other`. Optional notes from the customer. ```json Request example theme={null} { "reason": "Too expensive right now", "reason_category": "price", "notes": "Customer started cancellation from storefront" } ``` ### Response Cancellation case ID. Initial case status. Linked subscription ID. Submitted churn reason. Submitted reason category. *** ## GET /store/customers/me/subscriptions/:id/payment-methods Lists the authenticated customer's saved payment methods for the subscription's payment provider. ### Path parameters Subscription ID. Must belong to the authenticated customer. ### Response The payment provider ID associated with the subscription, or `null` if none is configured. Payment method identifier. Payment provider identifier. Type of payment method (e.g. `card`). Card brand (e.g. `visa`, `mastercard`). Last 4 digits of the card number. Card expiration month. Card expiration year. Unix timestamp of when the payment method was created. Whether this is the payment method currently assigned to the subscription. ```json Response example theme={null} { "payment_provider_id": "pp_stripe_stripe", "payment_methods": [ { "id": "pm_123", "provider_id": "pp_stripe_stripe", "type": "card", "brand": "visa", "last4": "4242", "exp_month": 4, "exp_year": 2030, "created_at": 1700000000, "is_current": true } ] } ``` ### Errors | Code | Error | Meaning | | ----- | ----------- | ---------------------------------------------------------------------------- | | `404` | `not_found` | Subscription does not exist or does not belong to the authenticated customer | *** ## POST /store/customers/me/subscriptions/:id/payment-method Changes the payment method the subscription renews with. ### Path parameters Subscription ID. Must belong to the authenticated customer. ### Body parameters Identifier of the saved payment method. Payment provider identifier. Optional if the subscription already has a configured provider. ```json Request example theme={null} { "payment_method_id": "pm_123", "provider_id": "pp_stripe_stripe" } ``` ### Response Returns the refreshed subscription detail payload, matching `GET /store/customers/me/subscriptions/:id`. ### Errors | Code | Error | Meaning | | ----- | -------------- | ----------------------------------------------------------------------------------------- | | `400` | `invalid_data` | Invalid body payload, unknown payment method, or payment method not owned by the customer | | `404` | `not_found` | Subscription does not exist or does not belong to the authenticated customer | | `409` | `conflict` | Subscription status does not allow updating the payment method | # Activity Log Source: https://docs.reorderjs.com/concepts/activity-log The Activity Log is an append-only audit trail of business-significant subscription events across renewals, dunning, cancellations, and subscription lifecycle changes. The Activity Log is a cross-domain audit trail that records business-significant events for each subscription. It aggregates events from Subscriptions, Renewals, Dunning, and Cancellation & Retention into a single operator-facing view. The log is append-only — existing records are never modified. State evolution is represented by new entries, not by mutating older ones. ## What gets logged The Activity Log records operator-facing outcomes, not internal diagnostics. Logged events include: **Subscription events** * Subscription created during storefront checkout * Subscription paused, resumed, or canceled * Plan change scheduled * Shipping address updated * Next delivery skipped **Renewal events** * Renewal approval approved or rejected * Force renewal requested * Renewal succeeded or failed **Dunning events** * Dunning started * Retry executed * Case recovered or unrecovered * Retry schedule updated manually **Cancellation & Retention events** * Cancellation case started * Retention offer applied * Cancellation reason updated * Cancellation finalized Low-level operational details — such as scheduler internals, lock diagnostics, payment provider payloads, and batch summaries — are not logged here. Those stay in structured operational logs. ## Event record Each log entry captures: | Field | Purpose | | ------------------------ | --------------------------------------------------------------------------------- | | **Event type** | The specific event from the taxonomy above | | **Actor** | Who or what triggered the event: `user`, `customer`, `system`, or `scheduler` | | **Previous / new state** | A compact before/after summary of the relevant fields that changed | | **Changed fields** | A structured diff showing field-level before and after values | | **Reason** | An optional operator-facing explanation (e.g. cancellation reason, approval note) | Display labels such as subscription reference, customer name, product title, and variant title are stored as snapshots on the record. This keeps the audit trail readable even if the underlying entities change later. ## Admin surfaces The Activity Log is accessible in two places: **Global Activity Log page** (`/subscriptions/activity-log`) — a cross-subscription audit view with filtering by event type, actor, and date range. Quick presets let you filter to a specific domain (Subscriptions, Renewals, Dunning, Cancellations) in one click. Clicking any row opens a detail drawer with the full event payload. **Subscription detail timeline** — an embedded Activity Log section on each subscription detail page, scoped to that subscription's history. Useful for reviewing the full lifecycle of one subscription without leaving its detail view. One common first event in that timeline is `subscription.created`, recorded when a customer completes subscription checkout through the storefront. ## Retention The Activity Log has no automatic purge or archival policy. Log records are treated as a business audit trail and are retained indefinitely. If a cleanup requirement arises in the future, it will be implemented as an explicit maintenance operation with clearly documented semantics. # Analytics Source: https://docs.reorderjs.com/concepts/analytics Analytics gives operators a reporting dashboard for recurring-commerce KPIs, trends, and exports — built primarily on daily subscription snapshots. Analytics is a reporting layer that aggregates data from across the plugin and exposes it as KPI cards, trend charts, and exportable datasets. It is read-only — it does not own or modify subscription, renewal, dunning, or cancellation state. Instead, it derives its outputs from those domains and precomputes daily snapshots for fast, consistent reads. ## KPI cards The Analytics page shows four key performance indicators for the selected date range and filters: | KPI | What it measures | | ------------------------ | ----------------------------------------------------------------------- | | **MRR** | Monthly-normalized recurring revenue from active subscriptions | | **Churn Rate** | Canceled subscriptions in the period divided by the average active base | | **LTV** | Derived as `MRR / churn_rate` — null when either input is unavailable | | **Active Subscriptions** | Count of subscriptions currently in `active` status | `MRR` and `LTV` are currency-aware. If your dataset spans multiple currencies without normalization, both values will be shown as unavailable rather than producing a misleading total. `Churn Rate` and `Active Subscriptions` are currency-independent. ## Filters All KPI cards, most trend series, and exports respond to the same set of filters: * **Date range** — `date_from` and `date_to`, both inclusive * **Status** — filter by subscription lifecycle status * **Product** — filter by product ID * **Frequency** — filter by billing cadence (e.g. weekly, monthly, yearly) * **Group by** — bucket trend data by `day`, `week`, or `month` All date calculations use UTC. ## Trend chart The trend chart visualizes one metric over the selected date range. * `MRR`, `Churn`, and `LTV` are grouped by the selected `group_by` value. * `created_subscriptions_count` is a special-case trend sourced directly from `subscription.created_at`. * The created-subscriptions trend always renders daily UTC buckets and zero-fills missing days inside the range. ## Export The Analytics page supports on-demand export of the currently visible reporting slice: | Format | Behavior | | ------ | --------------------------------------------------------------------------- | | `CSV` | Flat rows matching the active filters and bucket semantics | | `JSON` | Structured export payload with the same column ordering as the API response | Export is synchronous — it runs on demand and always reflects the currently active filters. ## How snapshots work Analytics precomputes one snapshot row per subscription per day. These daily snapshots power KPI queries, the snapshot-backed trend series, and exports — the dashboard does not recompute everything live from operational data on each request. ### Implemented exception The created-subscriptions trend does not come from `subscription_metrics_daily`. It is read directly from `subscription.created_at` because the required business question is simpler: how many subscriptions were created on each day in the selected range. Snapshots are rebuilt automatically by a daily scheduled job, with a short lookback window to self-heal recent changes. They can also be triggered incrementally after significant domain events such as subscription resumes, cancellation finalizations, or renewal executions that affect revenue data. If the analytics dashboard shows stale data after a significant operational change, a manual rebuild can be triggered from the Admin. The rebuild covers any date range and produces the same output as the scheduled job. # Background Jobs Source: https://docs.reorderjs.com/concepts/background-jobs The four scheduled jobs Reorder registers on your Medusa server and what each one does. Reorder registers four background jobs on your Medusa server at install time. They run automatically — no additional setup is required — but understanding what each one does, how often it runs, and how failures are handled is important for operating a production subscription store. There is no Admin UI dashboard for job status. Job activity is emitted as structured log entries to your Medusa logger and can be piped to any log aggregation system you already use. ## Overview | Job name | Schedule | Purpose | | ------------------------------------------ | ------------------ | --------------------------------- | | `process-renewal-cycles` | Every 5 minutes | Execute due subscription renewals | | `process-dunning-retries` | Every 5 minutes | Retry failed renewal payments | | `process-analytics-daily-snapshots` | Daily at 00:00 UTC | Rebuild analytics data | | `process-cancellation-operational-metrics` | Every hour | Aggregate cancellation metrics | *** ## process-renewal-cycles **Schedule:** `*/5 * * * *` — runs every 5 minutes. This is the core renewal engine. On each execution it queries for all `RenewalCycle` records that are due — specifically those with status `scheduled` or `failed` where `scheduled_for` is in the past. It then processes them in batches of 20. **What it does for each cycle:** 1. Checks whether approval is required. If `approval_required` is true and the cycle has not been approved, it is skipped until it is. 2. Runs `processRenewalCycleWorkflow`, which creates a Medusa order and charges the customer via the configured payment provider. 3. On success the renewal cycle transitions to `succeeded` and the next cycle is scheduled. 4. On payment failure the cycle transitions to `failed` and a `DunningCase` is opened. **Concurrency:** Individual renewal cycles are idempotent. If two job instances attempt to process the same cycle simultaneously, the second attempt is detected and logged as `blocked` (a warning, not an error). No distributed lock is used at the job level — cycles themselves are protected. **Log events emitted:** * `renewal.job` — job start and completion summary (scanned, processed, succeeded, failed, blocked counts) * `renewal.job.discovery` — how many due cycles were found * `renewal.job.batch` — per-batch progress * `renewal.job.cycle` — per-cycle outcome with duration *** ## process-dunning-retries **Schedule:** `*/5 * * * *` — runs every 5 minutes. Handles payment recovery for subscriptions that failed to renew. On each execution it queries for all `DunningCase` records whose next retry is due, processes them in batches of 20. **What it does for each case:** 1. Runs `runDunningRetryWorkflow`, which attempts to charge the customer again using the same payment provider. 2. On success the dunning case is marked `recovered` and the subscription is returned to `active`. 3. On failure the next retry is scheduled according to the dunning configuration (retry intervals and max attempts are set in [Subscription Settings](/configuration/subscription-settings)). 4. When all retry attempts are exhausted (or a permanent failure is detected) the case is marked `unrecovered` and the subscription remains in `past_due`. **Concurrency:** Protected by a distributed lock (`jobs:dunning-retries`). If a previous execution is still running when the next one fires, the new execution is skipped silently. This prevents double-charging on slow payment provider responses. **Log events emitted:** * `dunning.job` — job start and completion with aggregate stats (recovered, rescheduled, unrecovered, avg attempts, recovery rate, avg time to recover) * `dunning.job.discovery` — how many due cases were found * `dunning.job.batch` — per-batch progress * `dunning.job.case` — per-case outcome with attempt number and duration *** ## process-analytics-daily-snapshots **Schedule:** `0 0 * * *` — runs once daily at 00:00 UTC. Rebuilds the daily snapshot rows that power the Analytics dashboard. On each run it rebuilds the last **3 days** of snapshots (today plus the two preceding days) to self-correct any snapshots that may have been incomplete due to late-arriving data or a job failure. **What it does:** 1. Runs `rebuildAnalyticsDailySnapshotsWorkflow` over the resolved 3-day window. 2. For each day in the window it scans all subscriptions and upserts the snapshot row — MRR, active count, churn count, new subscriptions, and related KPIs. 3. The analytics dashboard reads exclusively from these snapshot rows, not from live subscription data. If this job does not run, the dashboard will show stale data. **Concurrency:** Protected by a distributed lock (`jobs:analytics-daily-snapshots`). Concurrent executions are skipped. If you need to rebuild analytics for a longer historical window — for example after a data migration or a multi-day outage — you can trigger a manual rebuild via `POST /admin/subscription-analytics/rebuild`. **Log events emitted:** * `analytics.job` — job start and completion (processed days, processed subscriptions, upserted rows, skipped rows, any failed or blocked days) *** ## process-cancellation-operational-metrics **Schedule:** `0 * * * *` — runs every hour. A read-only metrics aggregation job. It does not create or modify any records — it scans cancellation data for the past **24 hours** and emits structured log entries with operational metrics. Its primary purpose is to enable alerting on churn spikes. **What it reports:** * Total cancellation cases, terminal cases, cancelled and retained counts * Pause count, churn rate, and offer acceptance rate * Top cancellation reason categories for the window * Spike detection: if any reason category increases by 5 or more cases compared to the previous equivalent window, the log entry is emitted at `warn` level with `alertable: true` **What it does NOT do:** It does not write to the database, update any subscription records, or affect the Admin UI. The metrics exist only as log entries. **Concurrency:** Protected by a distributed lock (`jobs:cancellation-operational-metrics`). **Log events emitted:** * `cancellation.job` — completed metrics summary including churn rate, offer acceptance rate, top reason categories, and spike signal if detected *** ## Schedules and Configuration Job schedules are defined in code and are not configurable through the Admin UI or `medusa-config.ts`. The defaults are: ``` process-renewal-cycles */5 * * * * (every 5 min) process-dunning-retries */5 * * * * (every 5 min) process-analytics-daily-snapshots 0 0 * * * (daily at midnight UTC) process-cancellation-operational-metrics 0 * * * * (every hour) ``` To change a schedule you need to modify the `config.schedule` export in the corresponding job file and rebuild the plugin. ## Monitoring Since there is no dashboard, the recommended approach is to forward Medusa server logs to a log aggregation tool (Datadog, Grafana Loki, Logtail, etc.) and build alerts on the structured fields: * `outcome: "failed"` or `alertable: true` on any job event * `failure_kind: "unexpected_error"` on any job event * Missing `outcome: "completed"` entries for `analytics.job` — indicates the daily job did not run * `outcome: "blocked"` on `renewal.job.cycle` is normal (idempotency guard) and should not be alerted # Cancellation & Retention Source: https://docs.reorderjs.com/concepts/cancellation-retention Cancellation cases let you run retention offers before finalizing a cancellation. Learn how pause, discount, and bonus offers work and when to finalize. When a customer wants to cancel their subscription, Reorder gives you the opportunity to intervene before the cancellation is finalized. A cancellation case captures the customer's intent to cancel and opens a structured workflow where you can apply retention offers — such as a temporary pause, a discount, or a bonus — to change the customer's mind. If no offer works, you finalize the cancellation with a recorded reason. ## When a cancellation case is created A cancellation case can be started for any subscription in `active`, `paused`, or `past_due` status. Subscriptions that are already `cancelled` cannot open a new case. Cases are created in two ways: * **Store API**: A customer initiates a cancellation request through `POST /store/customers/me/subscriptions/:id/cancellation`. This creates a case and surfaces it in the Admin cancellations queue for operator handling. * **Admin**: An operator opens a case directly from the subscription detail page. Only one active cancellation case can exist per subscription at a time. ## Cancellation case states | Status | What it means | | ---------- | ----------------------------------------------------------------------- | | `open` | The case is active and awaiting a retention decision or finalization | | `paused` | A pause retention offer was accepted — the subscription is now paused | | `retained` | A discount or bonus offer was applied — the subscription remains active | | `canceled` | The cancellation was finalized — the subscription is now cancelled | ## Retention offer types Before finalizing a cancellation, you can apply one or more retention offers to try to keep the subscriber. Each offer is recorded as a retention offer event in the case history. | Offer type | What it does | | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `pause` | Pauses the subscription for a defined period. The case closes as `paused`. | | `discount` | Applies a discount to future renewals. The case closes as `retained` and the subscription stays active. | | `bonus` | Adds a bonus — such as a free product or credit — to the next renewal. The case closes as `retained` and the subscription stays active. | Applying a pause offer closes the cancellation case as `paused`. Applying a discount or bonus offer closes the case as `retained`. In both outcomes, the subscription is not cancelled. ## Finalizing a cancellation If no retention offer resolves the case, you finalize the cancellation from the case detail page. Finalization requires a churn reason to be recorded before it proceeds. When you finalize: * The subscription moves to `cancelled`. * `cancel_effective_at` is set. * `next_renewal_at` is cleared, stopping all future renewal scheduling. * The case closes as `canceled`. ## Cancellation reason categories When you record or update a churn reason on a case, you classify it into a reason category. These categories are used for reporting and help surface churn patterns in the Admin analytics. Common categories include product dissatisfaction, pricing concerns, and temporary pauses versus permanent cancellations. You can update the reason and category on an open case at any time without affecting the case status. ## Coexistence with dunning A cancellation case and a dunning case can be active for the same subscription simultaneously. A subscriber in `past_due` status — where dunning is actively retrying the payment — can still enter a retention flow or be finalized for cancellation. Dunning owns the payment recovery process; the cancellation case owns the churn handling process. Neither domain takes over the other's state. Finalizing a cancellation while a dunning case is active will cancel the subscription, but the dunning case retains its own closure flow. You will still need to resolve the dunning case separately. ## Store API self-service cancellation Customers can initiate their own cancellation request through the Store API without needing Admin intervention to start the process. The `POST /store/customers/me/subscriptions/:id/cancellation` endpoint creates a cancellation case and routes it to the Admin queue, where your team can review it and apply a retention offer or proceed with finalization. This flow keeps the customer-facing experience smooth while giving your team control over the final outcome. # Dunning Source: https://docs.reorderjs.com/concepts/dunning Dunning automatically retries failed subscription payments on a configurable schedule. Learn how cases work, what statuses mean, and how to resolve them. Dunning is the process of automatically retrying a payment that failed during renewal. When a renewal cycle fails because of a payment error — such as a declined card or insufficient funds — Reorder opens a dunning case for that subscription and begins retrying the payment on a scheduled interval. The goal is to recover the payment without requiring manual intervention for every failure. ## When dunning starts Dunning starts automatically when a renewal cycle fails after the renewal order has already been created. The failure must be payment-qualified — that is, caused by a payment session, authorization, or capture error — rather than a validation or eligibility issue. When dunning starts: * The subscription status moves to `past_due`. * A dunning case is created, linked to the originating renewal cycle and its order. * The retry schedule is applied and the first `next_retry_at` is set. Only one active dunning case can exist per subscription at a time. ## Dunning case statuses | Status | What it means | | ---------------------------- | --------------------------------------------------------------------------------------------------- | | `open` | The case has just been created and no retry has run yet | | `retry_scheduled` | The last retry failed with a retryable error and a future retry is scheduled | | `retrying` | A retry is currently in progress | | `awaiting_manual_resolution` | The case cannot continue automatic retries and requires a manual decision | | `recovered` | The payment was successfully collected and the subscription is active again | | `unrecovered` | The case is closed without recovery — either due to exhausted retries or a terminal payment failure | ## Retry intervals and max attempts The retry schedule defines how long Reorder waits between each payment attempt. Intervals are stored in minutes. The default policy applies a fixed-interval schedule, but you can override the schedule for an individual case from the Admin. The `max_attempts` field sets the total number of retry attempts allowed. When the attempt count reaches the maximum, the case moves to `awaiting_manual_resolution` if no terminal failure has occurred, or closes as `unrecovered` if the failure is permanent. Retryable errors include failures like `insufficient_funds`, generic declines, and temporary provider errors. Terminal errors — such as expired payment details or a missing payment method — close the case immediately as `unrecovered` without scheduling further retries. ## Recovery flow When a dunning retry succeeds, Reorder closes the case as `recovered` and restores the subscription to `active`. No further retries run. When the case cannot be recovered automatically, you have two manual resolution options from the dunning case detail page: | Action | What it does | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Mark recovered** | Closes the case as `recovered` and restores the subscription to `active`. Use this when payment was collected outside the normal retry flow. | | **Mark unrecovered** | Closes the case as `unrecovered`, leaving the subscription in `past_due`. Use this when you have confirmed the debt cannot be collected. | Both actions require a reason when marking unrecovered. ## Manual retry now You can trigger a payment retry immediately from the dunning case detail page without waiting for the next scheduled `next_retry_at`. The retry-now action runs the same payment retry workflow as the scheduler — it does not bypass any validation. After a manual retry: * If payment succeeds, the case closes as `recovered` and the subscription returns to `active`. * If the error is retryable, the case stays in `retry_scheduled` and the next scheduled retry is preserved. * If the error is terminal, the case closes as `unrecovered`. ## Coexistence with cancellation cases An active dunning case and an active cancellation case can exist for the same subscription at the same time. A `past_due` subscription is eligible for a retention flow or final cancellation while payment recovery is still in progress. Each domain — dunning and cancellation — manages its own state independently. See [Cancellation flows and retention offers](/concepts/cancellation-retention) for details on how cancellations work alongside dunning. # Medusa Integration Source: https://docs.reorderjs.com/concepts/medusa-integration How Reorder connects to Medusa's native modules — products, customers, orders, carts, and payments. Reorder is built as a native Medusa plugin. Rather than managing its own product catalog, customer records, or payment processing, it links to the modules Medusa already provides. This means subscription data is always connected to the same products, customers, and orders your store already uses. ## Module Links Reorder defines six module links that connect subscription records to Medusa core data. These links are established using Medusa's `defineLink` API and resolve through the standard query interface. | Reorder model | Medusa module | Cardinality | Description | | -------------- | ------------- | ----------- | --------------------------------------------------------- | | `Subscription` | `Customer` | many-to-one | One customer can own many subscriptions | | `Subscription` | `Cart` | one-to-one | The cart from the subscription checkout | | `Subscription` | `Order` | one-to-many | Original checkout order and all subsequent renewal orders | | `RenewalCycle` | `Order` | one-to-one | The Medusa order generated for each renewal cycle | Because these are standard module links, you can resolve them using Medusa's `query.graph()` from any workflow, API route, or subscriber — no Reorder-specific query layer is needed. Product and variant data is stored directly on the `Subscription` record as `product_id`, `variant_id`, and a `product_snapshot` field. Link definitions for `Product` and `ProductVariant` exist in the Reorder codebase but those link records are not populated during normal operation — use the snapshot fields to read product context. ## Checkout Flow When a customer subscribes through checkout, Reorder synchronizes cart pricing first, then wraps Medusa's native `completeCartWorkflow`. The subscription record is created and linked to the resulting order in the same transaction. After checkout completes: * The subscription cart pricing is synchronized for the selected cadence before payment continues. * A `Subscription` record is created in Reorder's module. * A `subscription.created` Activity Log event is appended with the storefront customer recorded as the actor. * The subscription is linked to the `Order` produced by `completeCartWorkflow`. * The subscription is linked to the `Customer` and `Cart` via module links. * Product and variant data from the cart line items is stored as a snapshot on the subscription record. * The first `RenewalCycle` is scheduled based on the plan's billing frequency. This means the initial subscription order appears in Medusa's standard orders list alongside all other orders — no separate order management surface is needed. ## Payment Module Integration Reorder uses Medusa's payment module for all money movement. It does not implement its own payment provider or store payment credentials. **Renewals** — when a renewal cycle executes, Reorder creates a new order using `createOrderWorkflow`, then calls `createOrUpdateOrderPaymentCollectionWorkflow` and `createPaymentSessionsWorkflow` to collect payment. Authorization and capture are handled via the payment module service directly. **Dunning** — when a renewal payment fails, Reorder opens a `DunningCase` and schedules retry attempts. Each attempt re-uses `createOrUpdateOrderPaymentCollectionWorkflow` and `createPaymentSessionsWorkflow` against the same renewal order. Attempt records are stored on the `DunningCase`. Because payment processing goes through Medusa's standard workflows, any payment provider that works with your Medusa store automatically works with Reorder subscriptions and renewals. ## Orders Integration Every renewal generates a standard Medusa order. These orders appear in the Admin orders list and can be managed, refunded, or fulfilled using the same tools as any other order. In addition, Reorder adds a widget to the Order Details page in Admin that surfaces the subscription context for any order that was created by a subscription renewal. See [Orders Widget](/admin/orders) for details. The order widget reads subscription context from Reorder's own Admin summary endpoint and can show: * linked subscription reference * status * frequency label * discount label from `pricing_snapshot` * projected next renewal date # Plans & Offers Source: https://docs.reorderjs.com/concepts/plans-offers Plan offers control which products and variants are subscribable, what billing frequencies are available, and what discounts or trials apply. A plan offer is the configuration record that makes a product or variant subscribable. It defines which billing frequencies are available to customers, what discount (if any) applies to each frequency, and additional rules like trial periods and minimum commitment cycles. Without an active plan offer, a product cannot be added to a subscription checkout. ## Product scope vs. variant scope You can configure a plan offer at the product level or at an individual variant level. When both exist for the same product, the variant-level offer always wins — it fully replaces the product-level configuration for that variant. No fields are merged between the two records. | Scope | When it applies | | --------- | ------------------------------------------------------------------------------------------------ | | `product` | Applies to the product as a whole when no variant-level offer is active for the selected variant | | `variant` | Applies only to one specific variant and takes precedence over any product-level offer | If a variant-level offer is disabled or does not exist for a variant, Reorder falls back to the product-level offer. If neither is active, the product is not subscribable. ## Allowed frequencies Each plan offer defines one or more allowed billing frequencies. A frequency is a combination of an interval and a positive integer value: | Interval | Example values | Example meaning | | -------- | -------------- | ------------------------------------------ | | `week` | `1`, `2`, `4` | Every week, every 2 weeks, every 4 weeks | | `month` | `1`, `2`, `3` | Every month, every 2 months, every quarter | | `year` | `1` | Every year | Each frequency must use a unique `interval:value` combination within the same offer. Duplicates are rejected. ## Per-frequency discounts You can attach a discount to each frequency in the offer. Customers who choose a longer or preferred cadence can receive a better price. | Field | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------- | | `interval` | The frequency interval the discount applies to | | `frequency_value` | The frequency value the discount applies to | | `type` | `percentage` or `fixed` | | `value` | The discount amount — a percentage between 0 and 100 for `percentage`, or a positive fixed amount for `fixed` | Discounts are optional and can be defined for any subset of the allowed frequencies. A discount can only be attached to a frequency that is already present in `allowed_frequencies`. ## Offer rules Each plan offer can include a rules object with three settings: ### Minimum cycles `minimum_cycles` sets a minimum number of billing cycles before a customer can cancel. For example, setting `minimum_cycles: 3` means a subscriber must complete at least three paid renewals before the subscription can be cancelled through a standard cancellation flow. ### Trial settings | Field | Description | | --------------- | ------------------------------------------------------------------------------------ | | `trial_enabled` | Whether a free trial period applies to new subscribers | | `trial_days` | The number of trial days (required when `trial_enabled` is `true`, otherwise `null`) | When a trial is active, the subscription is marked as a trial and `trial_ends_at` is set. Billing starts after the trial period ends. ### Stacking policy The stacking policy controls how the subscription discount interacts with other discounts in your store: | Policy | Behavior | | --------------------------------- | ------------------------------------------------------------------------------------------------- | | `allowed` | The subscription discount stacks with other active discounts | | `disallow_all` | No other discounts apply alongside the subscription discount | | `disallow_subscription_discounts` | Other subscription-specific discounts are blocked, but non-subscription discounts may still apply | ## Enabled and disabled states Every plan offer has an `is_enabled` flag. Only enabled offers are considered during effective config resolution. Disabling an offer removes it from the subscribable configuration for new subscriptions without deleting the record — you can re-enable it at any time from the Admin. Disabling an offer does not affect subscriptions that are already active. It only prevents new subscriptions from using that offer configuration. ## Effective config resolution When Reorder resolves the subscription configuration for a product and variant — for example, during a plan change or a PDP offer lookup — it follows this order: 1. Look for an active variant-scoped offer for the requested variant. 2. If none exists, look for an active product-scoped offer for the product. 3. If neither exists, the product is not subscribable. The winning record provides all configuration fields. Reorder does not merge frequencies, discounts, or rules across the product-level and variant-level records. You can verify the resolved configuration from the offer detail page in the Admin, which shows the effective config summary alongside the source record. # Renewals Source: https://docs.reorderjs.com/concepts/renewals A renewal cycle represents one scheduled billing event for a subscription. Learn how cycles are executed, approved, failed, and how skipping a cycle works. A renewal cycle is a single scheduled billing event for a subscription. When a subscription is created, Reorder schedules the first renewal cycle automatically. After each successful renewal, the next cycle is created and the subscription's `next_renewal_at` date advances. Every renewal cycle has its own status, attempt history, and — when applicable — approval state. ## Renewal cycle statuses | Status | What it means | | ------------ | -------------------------------------------------------------------------------------- | | `scheduled` | The cycle is due in the future and waiting to be processed | | `processing` | The execution workflow is currently running for this cycle | | `succeeded` | The renewal completed successfully and a new order was created | | `failed` | The renewal attempt failed — either due to a payment error or another processing issue | A `failed` cycle that failed because of a payment error will trigger dunning automatically. See [Dunning](/concepts/dunning) for how payment recovery works. ## How renewal execution works Renewal cycles are processed by a background scheduler that runs every five minutes. The scheduler discovers all cycles with status `scheduled` or `failed` whose `scheduled_for` date is in the past and runs the execution workflow for each. During execution, the workflow: 1. Validates that the linked subscription is eligible (not paused or cancelled). 2. Checks whether approval is required and has been granted. 3. Re-validates the current plan offer policy, including any pending plan changes. 4. Creates the renewal order using the subscription's current plan and shipping address. 5. Applies any approved pending plan change to the subscription's active cadence. 6. Updates `next_renewal_at` and creates the next scheduled renewal cycle. Paused and cancelled subscriptions are not eligible for renewal execution. Renewal cycles for ineligible subscriptions are skipped by the scheduler. ## The approval flow Some renewal cycles require admin approval before they can execute — for example, when a plan change is pending and your configuration requires human review before the change takes effect. When approval is required, the cycle will not run until an admin approves it. Approval statuses: | Status | What it means | | ---------- | ----------------------------------------------------------------------- | | `pending` | The cycle is waiting for an approval decision | | `approved` | The changes have been approved and the cycle may proceed | | `rejected` | The changes were rejected — the cycle will not apply the pending change | You approve or reject a cycle from its detail page in the Admin renewals queue. Rejection requires a reason; approval does not. ## Manual force renewal You can run a renewal cycle immediately from the Admin regardless of its scheduled date using the force renewal action. Force renewal uses the same execution workflow as the scheduler — the only difference is that it runs on demand. If a cycle requires approval, you must approve it before a force renewal is allowed. ## What happens when a renewal fails When a renewal cycle fails after successfully creating the renewal order, Reorder starts a dunning case automatically. The subscription moves to `past_due` and the dunning scheduler begins retrying the payment according to the configured retry schedule. If the renewal fails before an order is created — for example, due to a validation or eligibility error — dunning is not started. The cycle remains in `failed` state and can be retried manually using force renewal. ## Skipping a cycle When you or a customer enables skip next cycle on a subscription, the upcoming renewal cycle is not cancelled — instead, it is skipped during execution and rescheduled for the following period. The `effective_next_renewal_at` field shows the projected delivery date after the skip, while `next_renewal_at` remains the internal billing anchor. To skip the next delivery: * Admin users can use the subscription detail page. * Customers can call `POST /store/customers/me/subscriptions/:id/skip-next-delivery`. The `skip_next_cycle` flag resets to `false` after the skip is applied. # Subscriptions Source: https://docs.reorderjs.com/concepts/subscriptions A subscription links a customer to a product variant on a recurring schedule. Learn about statuses, lifecycle transitions, and key fields. A subscription in Reorder connects a customer to a specific product variant and bills them on a recurring schedule — for example, every month or every two weeks. When a customer completes a subscription checkout, Reorder creates a subscription record, sets the first renewal date, and schedules the first renewal cycle automatically. From that point on, the Admin dashboard and the Store API give you full visibility and control over the subscription's lifecycle. ## What a subscription tracks Each subscription holds everything Reorder needs to manage the recurring relationship: | Field | Description | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `reference` | Human-readable identifier shown in the Admin (for example, `SUB-001`) | | `status` | Current lifecycle state: `active`, `paused`, `past_due`, or `cancelled` | | `frequency_interval` | The time unit for billing cadence: `week`, `month`, or `year` | | `frequency_value` | The number of intervals between billings (for example, `2` with `month` means every 2 months) | | `next_renewal_at` | The billing anchor date used by the renewal scheduler | | `effective_next_renewal_at` | The projected next delivery date shown in Admin and storefront — differs from `next_renewal_at` when the next cycle is skipped | | `skip_next_cycle` | When `true`, the upcoming renewal is skipped and rescheduled for the following cycle | | `pending_update_data` | A scheduled plan or cadence change that will be applied at the next eligible renewal | | `started_at` | When the subscription became active | | `last_renewal_at` | When the most recent successful renewal ran | | `payment_context` | The payment provider and reusable payment method references captured at checkout for off-session renewals | ## Subscription statuses | Status | What it means | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `active` | The subscription is billing normally. Renewals run on schedule. | | `paused` | Billing is temporarily stopped. No renewals run while paused. The subscription resumes when you or the customer triggers a resume action. | | `past_due` | A renewal payment failed and dunning is active. The subscription stays in this state until the payment recovers or the case is closed. | | `cancelled` | The subscription has ended. No further renewals will run. | A subscription moves to `past_due` automatically when a renewal payment fails and qualifies for dunning. It returns to `active` when the payment is successfully recovered. ## Lifecycle transitions Subscriptions move between statuses through explicit actions, not automatically (except for the `past_due` transition, which happens during renewal execution): | From | To | Trigger | | ---------- | ----------- | -------------------------------------------- | | `active` | `paused` | Pause action (Admin or Store API) | | `active` | `past_due` | Renewal payment failure starts dunning | | `active` | `cancelled` | Cancel action or finalized cancellation case | | `paused` | `active` | Resume action (Admin or Store API) | | `paused` | `cancelled` | Cancel action or finalized cancellation case | | `past_due` | `active` | Dunning recovery succeeds | | `past_due` | `cancelled` | Cancel action or finalized cancellation case | ## How subscriptions are created Subscriptions are created through the checkout flow. When a cart line item carries subscription metadata (frequency interval and frequency value), Reorder synchronizes subscription pricing on the cart and then completes the subscription purchase through `POST /store/carts/:id/subscribe` instead of the standard Medusa cart complete route. Reorder validates the subscription metadata, completes the cart into a standard Medusa order, creates the subscription record, schedules the first renewal cycle, and writes an initial `subscription.created` event to the Activity Log with the customer recorded as the actor. The checkout pricing adjustment is applied as a manual line-item adjustment identified by `provider_id = "subscription_discount"`. This keeps the subscription discount separate from Medusa promotion-code semantics while still allowing the discounted total to flow into payment and order creation. A cart must contain only subscription line items when using the subscribe endpoint. Mixed carts — combining subscription and one-time items — are not supported. ## Pending plan changes You can schedule a plan change on an active subscription using the schedule-plan-change action. The change is stored in `pending_update_data` and applied during the next eligible renewal cycle rather than immediately. The pending change includes the target variant, frequency, and an optional effective date. The Admin detail view shows a preview of the pending change alongside the current plan so you can review it before it takes effect. ## Payment methods and renewals When a customer checks out a subscription, Reorder captures the payment provider and a reusable payment method reference in the subscription's payment context. Future renewals use this payment context to charge the customer off-session without requiring them to re-enter card details. If a card expires or a renewal fails, customers and operators can change the saved payment method assigned to the subscription. Reorder lists only saved payment methods belonging to that customer with the same payment provider, preventing cross-provider mismatches. ## Admin and Store API actions Reorder exposes subscription management through both the Admin API and the Store API. **Admin API** (`/admin/subscriptions`): * List and search subscriptions * View full subscription detail * Pause, resume, and cancel * Schedule a plan change * Update the shipping address * Change the payment method **Store API** (`/store/customers/me/subscriptions`): * List and view the authenticated customer's subscriptions * Pause, resume, and change frequency * Change shipping address * Skip the next delivery * Swap to a different product variant * Retry a failed payment * View saved payment methods and change the renewal payment method * Initiate a cancellation request # Subscription Settings Source: https://docs.reorderjs.com/configuration/subscription-settings Configure global defaults for trials, dunning, renewals, and cancellations. New operations use these values; active cases keep their own configuration. Subscription settings give you one place to control the global defaults that Reorder applies when new subscription operations begin. You can adjust trial lengths, dunning retry schedules, renewal behavior, and cancellation posture without redeploying your store. Settings changes apply to **future operations and newly created process state only**. Existing active dunning cases, cancellation flows, and renewal cycles keep the configuration that was snapshotted when they were created. ## Accessing subscription settings In your Medusa Admin, navigate to **Settings → Subscription Settings** (`/app/settings/subscription-settings`). The page loads the current effective settings and lets you edit and save them in a single form. The settings page is split into four sections: * **Trial** — default trial period for new subscription operations * **Dunning** — retry schedule and maximum attempt count for new dunning cases * **Renewals** — default renewal behavior for newly started renewal cycles * **Cancellation defaults** — default posture when a new cancellation flow opens ## Configurable fields | Field | Type | Default | Description | | ------------------------------- | --------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `default_trial_days` | integer ≥ 0 | `0` | Number of trial days applied by default to new subscription operations. `0` means no default trial. | | `dunning_retry_intervals` | array of integers (minutes) | `[1440, 4320, 10080]` | Ordered retry schedule for newly created dunning cases, expressed in minutes. Must be strictly increasing with no duplicates. | | `max_dunning_attempts` | integer > 0 | `3` | Maximum number of retry attempts for newly created dunning flows. Must equal the number of intervals in `dunning_retry_intervals`. | | `default_renewal_behavior` | enum | `process_immediately` | Global default behavior when a new renewal operation begins. See [renewal behavior values](#renewal-behavior-values). | | `default_cancellation_behavior` | enum | `recommend_retention_first` | Global default posture when a new cancellation flow starts. See [cancellation behavior values](#cancellation-behavior-values). | ### Renewal behavior values | Value | Meaning | | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `process_immediately` | Treat new renewals as immediately processable from a global-settings perspective. Workflow-level validations and approval rules still apply. | | `require_review_for_pending_changes` | When a new renewal starts and there is reviewable change context (such as a pending plan update), default toward a review and approval path. | ### Cancellation behavior values | Value | Meaning | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `recommend_retention_first` | When a new cancellation flow opens, begin with retention-oriented handling — pause offers, discounts, or bonuses — before proceeding to cancellation. | | `allow_direct_cancellation` | When a new cancellation flow opens, allow a direct-cancellation path as the default posture. Cancellation validations still run. | ## Persisted state and the is\_persisted flag Before you save settings for the first time, Reorder serves built-in fallback defaults. These defaults are not stored in the database — they are assembled at read time. Once you save settings for the first time, Reorder creates a persistent singleton record and uses it as the runtime source of truth. The `is_persisted` field in the API response tells you which state you are in: | `is_persisted` | Meaning | | -------------- | ------------------------------------------------------------------------------ | | `false` | The response is built from fallback defaults. No settings have been saved yet. | | `true` | The response comes from the stored singleton. At least one save has occurred. | The info panel at the top of the settings page shows the current `version`, `updated_at`, and `updated_by` values so you can see when the record was last changed and by whom. ## The version field Every successful save increments the `version` field by one. Version starts at `0` for fallback defaults and becomes `1` after the first save. The `version` field is used for optimistic locking. When you submit a settings update through the API, you pass the `expected_version` you read to ensure your update is based on the latest state. If another update has occurred since your read, the API returns a `409 Conflict`. ## Updating settings via the Admin API You can read and update subscription settings programmatically using the Admin API. ### Get current settings ```bash theme={null} GET /admin/subscription-settings ``` This endpoint always returns a valid response. If no settings have been saved yet, it returns the fallback defaults with `is_persisted: false`. It never returns `404`. **Example response (fallback defaults):** ```json theme={null} { "subscription_settings": { "settings_key": "global", "default_trial_days": 0, "dunning_retry_intervals": [1440, 4320, 10080], "max_dunning_attempts": 3, "default_renewal_behavior": "process_immediately", "default_cancellation_behavior": "recommend_retention_first", "version": 0, "updated_by": null, "updated_at": null, "metadata": null, "is_persisted": false } } ``` ### Update settings ```bash theme={null} POST /admin/subscription-settings ``` Send only the fields you want to change. Omitted fields keep their current effective values. **Request body:** ```json theme={null} { "default_trial_days": 21, "dunning_retry_intervals": [1440, 4320, 10080], "max_dunning_attempts": 3, "expected_version": 0 } ``` Pass `expected_version: 0` for your first save. For subsequent saves, pass the `version` value returned by the most recent GET or POST response. **Example response (after first save):** ```json theme={null} { "subscription_settings": { "settings_key": "global", "default_trial_days": 21, "dunning_retry_intervals": [1440, 4320, 10080], "max_dunning_attempts": 3, "default_renewal_behavior": "process_immediately", "default_cancellation_behavior": "recommend_retention_first", "version": 1, "updated_by": "user_01ABC", "updated_at": "2026-04-03T14:00:00.000Z", "metadata": { "audit_log": [ { "action": "update_settings", "who": "user_01ABC", "when": "2026-04-03T14:00:00.000Z", "reason": "admin_save", "previous_version": 0, "next_version": 1, "change_summary": [ { "field": "default_trial_days", "from": 0, "to": 21 } ] } ], "last_update": { "action": "update_settings", "who": "user_01ABC", "when": "2026-04-03T14:00:00.000Z", "reason": "admin_save", "previous_version": 0, "next_version": 1, "change_summary": [ { "field": "default_trial_days", "from": 0, "to": 21 } ] } }, "is_persisted": true } } ``` ## Validation rules The API enforces these rules on every update request: * `default_trial_days` must be `>= 0` * `max_dunning_attempts` must be `> 0` * `dunning_retry_intervals` must contain positive integers only * `dunning_retry_intervals` must be strictly increasing with no duplicates * `max_dunning_attempts` must equal the number of intervals in `dunning_retry_intervals` * `default_renewal_behavior` must be one of the supported enum values * `default_cancellation_behavior` must be one of the supported enum values * `expected_version` must be `>= 0` A request that violates any of these rules returns `400 invalid_data`. A version mismatch returns `409 Conflict`. ## Next steps * [Plugin setup](/configuration/plugin-setup) — review the full plugin installation steps * [Dunning](/concepts/dunning) — understand how dunning cases consume these settings at create time * [Renewals](/concepts/renewals) — see how renewal behavior settings apply to new renewal cycles # Introduction Source: https://docs.reorderjs.com/introduction Reorder is an open-source Medusa plugin that adds subscriptions, renewals, dunning, retention flows, and analytics to your store. Reorder is an open-source plugin for [Medusa.js](https://medusajs.com) that turns your store into a full recurring commerce platform. It adds subscription management, configurable plans and offers, automated renewal orchestration, payment recovery through dunning, cancellation retention flows, and subscription analytics — all from the Medusa Admin. Reorder extends Medusa's native modules rather than replacing them. Subscriptions link directly to your existing products, customers, carts, and orders. Renewals generate standard Medusa orders and use your configured payment provider. Admin users can also move directly from a Medusa order to its linked subscription through the order-detail widget. No separate catalogs, customer records, or payment credentials to manage. ## Features Create, pause, resume, and cancel subscriptions. Track lifecycle state and upcoming renewals. Configure subscription frequencies, per-frequency discounts, trials, and minimum cycle rules. Scheduler-backed renewal execution with manual override, approval flows, and attempt history. Automatic payment retry scheduling with configurable intervals and manual recovery actions. Run retention flows — pause offers, discounts, and bonuses — before finalizing cancellations. Track MRR, churn rate, LTV, and active subscription counts with filterable trend charts. Ready to install? Follow the [Quick Start](/quickstart) guide. # Quick Start Source: https://docs.reorderjs.com/quickstart Install the Reorder plugin into your Medusa project and start managing subscriptions in minutes. ## Prerequisites Reorder is a plugin for an existing Medusa store. If you don't have one yet, [create a Medusa application](https://docs.medusajs.com/learn) first, then come back here. Once you have a running Medusa store, make sure it meets the version requirements: | Requirement | Minimum | Recommended | | ------------------ | ------- | ----------- | | `@medusajs/medusa` | `2.3.0` | `>= 2.4.0` | | Node.js | `20` | Latest LTS | Reorder is **not compatible** with Medusa 1.x. It relies on modules, workflows, and Admin UI extensions introduced in Medusa 2.x. ## Installation In your Medusa application, add `@reorderjs/reorder` using your preferred package manager. ```bash npm theme={null} npm install @reorderjs/reorder ``` ```bash yarn theme={null} yarn add @reorderjs/reorder ``` Open `medusa-config.ts` and add Reorder to the `plugins` array. ```ts medusa-config.ts theme={null} plugins: [ // other plugins { resolve: "@reorderjs/reorder", options: {}, }, ] ``` Apply Reorder's database migrations from your Medusa application directory. ```bash npm theme={null} npx medusa db:migrate ``` ```bash yarn theme={null} yarn medusa db:migrate ``` Run your Medusa server. Reorder will register its modules, API routes, scheduled jobs, and Admin UI extensions on boot. ```bash npm theme={null} npm run dev ``` ```bash yarn theme={null} yarn dev ``` Navigate to your Medusa Admin dashboard. You will see a **Subscriptions** entry in the sidebar with access to Plans & Offers, Renewals, Dunning, Cancellations, Activity Log, and Analytics. ## What gets added to your store Once the plugin is loaded, Medusa boots with the following additions from Reorder: | Component | What it provides | | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Admin UI extensions** | Dedicated pages for Subscriptions, Plans & Offers, Renewals, Dunning, Cancellations, Activity Log, Analytics, Subscription Settings, plus an order-detail subscription widget | | **Admin API routes** | Protected REST endpoints under `/admin/` for all subscription operations, settings, and analytics queries | | **Store API routes** | Customer-facing endpoints under `/store/` for subscription checkout and offer resolution | | **Scheduled jobs** | Renewal execution, dunning retry scheduling, and analytics roll-up jobs that run on configurable cadences | ## Next steps * [Core Concepts](/concepts/subscriptions) — understand the subscription lifecycle, plan model, and renewal flow * [Plans & Offers](/admin/plans-offers) — create your first subscription plan and attach it to a product * [Subscription Settings](/configuration/subscription-settings) — tune dunning intervals, trial defaults, and renewal behavior # Cart Source: https://docs.reorderjs.com/storefront/cart How to handle subscription state in the cart — displaying cadence, enforcing purchase mode, and blocking mixed carts. Once a customer adds a subscription item to their cart, the cart needs to reflect the subscription context clearly and enforce the constraints required by the checkout flow. ## Purchase mode The cart's purchase mode is derived from line item metadata. Three modes are possible: | Mode | Condition | | -------------- | -------------------------------------------------- | | `one-time` | No subscription line items in the cart | | `subscription` | All line items are subscription items | | `mixed` | Cart contains both subscription and one-time items | Use a helper like `getCartPurchaseMode(cart)` to derive the current mode from line item metadata and drive your cart UI accordingly. ## Displaying subscription items For each subscription line item, show the billing cadence alongside the product. The cadence is stored in the line item's metadata fields `frequency_interval` and `frequency_value`: ```typescript theme={null} const isSubscription = item.metadata?.is_subscription === true; const interval = item.metadata?.frequency_interval; // e.g. "month" const value = item.metadata?.frequency_value; // e.g. 1 ``` Use these fields to display context like "Delivered monthly" or "Every 3 months" directly in the cart row. ## Mixed cart guard Mixed carts are not supported in the current release. If the cart contains both subscription and one-time items, disable the checkout CTA and show a clear explanation to the customer. ```typescript theme={null} if (purchaseMode === "mixed") { // Disable checkout, show limitation message } ``` If a customer wants to purchase a subscription item and a one-time item together, they need to use separate carts and complete them through their respective checkout flows. ## Cart CTA Change the cart's call-to-action label based on the current purchase mode to set the right expectation before checkout: | Mode | Suggested CTA | | -------------- | ---------------------------------- | | `one-time` | "Proceed to checkout" | | `subscription` | "Proceed to subscription checkout" | | `mixed` | Disabled — show mixed cart message | ## Next step When the customer proceeds from a subscription cart, complete the order through the [Subscription Checkout](/storefront/subscription-checkout) flow instead of the standard Medusa cart-complete route. # Customer Portal Source: https://docs.reorderjs.com/storefront/customer-portal Use the Store API's customer subscription endpoints to let authenticated customers view, manage, and cancel their subscriptions from your storefront. Reorder exposes a full set of customer-facing subscription management endpoints under `/store/customers/me/subscriptions`. You can use these to build a self-service portal where customers can view their active subscriptions, change delivery frequency, update their address, skip a delivery, swap to a different product, retry a failed payment, pause or resume billing, and request cancellation — all without leaving your storefront. ## Authentication All customer subscription endpoints require an authenticated customer session. Include the customer's session cookie or a bearer token with each request: ```bash theme={null} Authorization: Bearer ``` Requests without valid authentication return `401`. Requests for subscriptions that belong to a different customer return `403`. ## Listing subscriptions Retrieve all subscriptions for the authenticated customer: ```bash theme={null} GET /store/customers/me/subscriptions ``` ```json theme={null} { "subscriptions": [ { "id": "sub_123", "reference": "SUB-001", "status": "active", "product_title": "Daily Vitamins", "variant_title": "60 capsules", "next_renewal_at": "2026-05-01T10:00:00.000Z", "active_cancellation_case": null } ] } ``` ## Viewing subscription detail Retrieve the full detail for a single subscription: ```bash theme={null} GET /store/customers/me/subscriptions/:id ``` The detail payload includes all fields from the list view, plus: | Field | Description | | --------------------------- | ----------------------------------------------------------------------------- | | `frequency_interval` | Billing cadence unit: `"week"`, `"month"`, or `"year"` | | `frequency_value` | Number of intervals between billings | | `effective_next_renewal_at` | Projected next delivery date, accounting for skipped cycles | | `last_renewal_at` | When the most recent renewal ran | | `shipping_address` | The address snapshot for this subscription's deliveries | | `payment_status` | Current payment status | | `payment_provider_id` | The payment provider handling this subscription | | `payment_method` | Summary of the current saved payment method (brand, last4, expiry), or `null` | | `payment_recovery` | Active dunning recovery case, if any | | `scheduled_plan_change` | Pending variant or cadence update, if one is queued | | `active_cancellation_case` | Active cancellation case, if one is open | ## Managing subscriptions All management actions return a refreshed subscription detail payload so your UI can update in place without a separate fetch. ### Pause Pause billing on a subscription. Optionally provide a reason and an effective date: ```bash theme={null} POST /store/customers/me/subscriptions/:id/pause ``` ```json theme={null} { "reason": "Taking a short break", "effective_at": "2026-04-15T10:00:00.000Z" } ``` ### Resume Resume a paused subscription. Optionally preserve the original billing anchor date: ```bash theme={null} POST /store/customers/me/subscriptions/:id/resume ``` ```json theme={null} { "resume_at": "2026-04-20T10:00:00.000Z", "preserve_billing_anchor": true } ``` ### Change frequency Schedule a cadence change for the next renewal. The current variant stays unchanged. The new cadence must be valid according to the active plan for this subscription: ```bash theme={null} POST /store/customers/me/subscriptions/:id/change-frequency ``` ```json theme={null} { "frequency_interval": "month", "frequency_value": 2, "effective_at": "2026-05-01T10:00:00.000Z" } ``` ### Change address Update the shipping address used for future deliveries: ```bash theme={null} POST /store/customers/me/subscriptions/:id/change-address ``` ```json theme={null} { "first_name": "Jane", "last_name": "Doe", "address_1": "Main Street 1", "city": "Copenhagen", "postal_code": "2100", "country_code": "dk" } ``` ### Skip next delivery Mark the next renewal cycle as skipped. The subscription resumes automatically at the following cycle. This endpoint takes no request body: ```bash theme={null} POST /store/customers/me/subscriptions/:id/skip-next-delivery ``` ### Swap product Schedule a product or variant swap. The target variant must belong to the same product and be allowed by the active plan. The change applies at the next eligible renewal: ```bash theme={null} POST /store/customers/me/subscriptions/:id/swap-product ``` ```json theme={null} { "variant_id": "variant_123", "frequency_interval": "month", "frequency_value": 1, "effective_at": "2026-05-01T10:00:00.000Z" } ``` ### Retry payment Trigger a manual payment retry for a subscription that is in a recoverable failed-payment state. Include an optional reason to log with the retry attempt: ```bash theme={null} POST /store/customers/me/subscriptions/:id/retry-payment ``` ```json theme={null} { "reason": "Customer requested immediate retry" } ``` This endpoint returns `409` if the subscription does not have a retry-eligible payment recovery case. Check `payment_recovery` in the subscription detail before surfacing this action to the customer. ### View saved payment methods Fetch the authenticated customer's saved payment methods for the subscription's payment provider: ```bash theme={null} GET /store/customers/me/subscriptions/:id/payment-methods ``` ```json theme={null} { "payment_provider_id": "pp_stripe_stripe", "payment_methods": [ { "id": "pm_123", "provider_id": "pp_stripe_stripe", "type": "card", "brand": "visa", "last4": "4242", "exp_month": 4, "exp_year": 2030, "created_at": 1700000000, "is_current": true } ] } ``` ### Change payment method Switch the subscription's renewal payment method to another saved payment method: ```bash theme={null} POST /store/customers/me/subscriptions/:id/payment-method ``` ```json theme={null} { "payment_method_id": "pm_123", "provider_id": "pp_stripe_stripe" } ``` The response returns the refreshed subscription detail payload with the updated `payment_method`. ### Request cancellation Start a cancellation case for the subscription. Provide a reason category and an optional free-text reason: ```bash theme={null} POST /store/customers/me/subscriptions/:id/cancellation ``` ```json theme={null} { "reason": "Too expensive right now", "reason_category": "price", "notes": "Customer started cancellation from storefront" } ``` The response returns a minimal cancellation case payload with `id`, `status`, `subscription_id`, and the submitted reason fields. The subscription is not immediately cancelled — the case enters the configured cancellation and retention workflow. ## Surfacing actions based on subscription status Not all actions are relevant for every subscription state. Use the subscription's `status` field to show the appropriate actions in your portal: | Status | Available actions | | ----------- | -------------------------------------------------------------------------------------------------------- | | `active` | Pause, change frequency, change address, change payment method, skip next delivery, swap product, cancel | | `paused` | Resume, change address, change payment method, cancel | | `past_due` | Retry payment, change payment method, change address, cancel | | `cancelled` | None | # Subscription Checkout Source: https://docs.reorderjs.com/storefront/subscription-checkout Use the subscribe endpoint to convert a prepared cart into a subscription at checkout, with frequency and offer data attached as line item metadata. When a customer selects a subscription offer on your storefront and proceeds to checkout, you complete their cart through a dedicated endpoint instead of the standard Medusa cart-complete route. Reorder validates the subscription metadata on the cart's line items, converts the cart into a standard Medusa order, creates the subscription record, and schedules the first renewal cycle — all in a single call. ## How subscription checkout works Call `POST /store/carts/:id/subscribe` in place of the standard cart-complete route when the cart contains a subscription item. Reorder reads the subscription configuration from line item metadata to determine the billing cadence and offer terms. ```bash theme={null} POST /store/carts/:id/subscribe ``` The endpoint returns the newly created subscription along with the completed order. On the first successful creation, Reorder also records a `subscription.created` activity-log event for that subscription. In Admin, operators will see the storefront customer as the actor for that first event. ## Required line item metadata Each subscription line item must include the following metadata fields: | Field | Type | Description | | -------------------- | ----------------------------- | ------------------------------------------ | | `is_subscription` | `boolean` | Marks the line item as a subscription item | | `frequency_interval` | `"week" \| "month" \| "year"` | The time unit for billing cadence | | `frequency_value` | positive integer | The number of intervals between billings | These fields are the source of truth for the subscription's cadence. Reorder reads them directly from the line item, regardless of any cart-level metadata. ## Optional cart metadata You can also set `purchase_mode` on the cart itself to make the intent explicit: | Field | Value | Description | | ----------------------------- | ---------------- | ------------------------------------------------ | | `cart.metadata.purchase_mode` | `"subscription"` | Signals that the cart is a subscription checkout | If `purchase_mode` is present, it must be set to `"subscription"`. Any other value returns a `400` error. ## Setting metadata before checkout Set line item and cart metadata when the customer confirms their subscription selection. The example below shows a typical pre-checkout call using the Medusa JS SDK: ```typescript theme={null} // Update the line item with subscription metadata await medusa.store.cart.updateLineItem(cartId, lineItemId, { metadata: { is_subscription: true, frequency_interval: "month", frequency_value: 1, }, }); // Optionally mark the cart as a subscription checkout await medusa.store.cart.update(cartId, { metadata: { purchase_mode: "subscription", }, }); // Complete checkout through the subscribe endpoint const { subscription, order } = await medusa.client.fetch( `/store/carts/${cartId}/subscribe`, { method: "POST" } ); ``` ## Constraints The subscribe endpoint enforces the following rules in the current release: * **Exactly one subscription line item** — the cart must contain a single subscription item. Multiple subscription line items are not supported. * **Quantity of 1** — the subscription line item must have a quantity of `1`. * **No mixed carts** — a cart cannot combine subscription and one-time-purchase items. Submit a separate cart for one-time items. A cart that violates any of these constraints returns a `400` error. Mixed carts are not supported. If a customer wants to purchase both a subscription item and a one-time item in the same session, use separate carts and complete them through their respective endpoints. ## Idempotency after cart completion The subscribe endpoint is safe to call more than once after the cart is already complete. If the order created from the cart is already linked to a subscription, Reorder returns the existing subscription instead of creating a duplicate. This means retries due to network issues or client-side double-submits will not produce extra subscriptions. # Subscription Offers Source: https://docs.reorderjs.com/storefront/subscription-offers Fetch the effective subscription offer for a product or variant to power frequency selectors, savings displays, and checkout validation on your storefront. Before a customer can subscribe, your product detail page needs to know which subscription frequencies are available, what discounts apply, and whether subscriptions are offered at all for that product or variant. The subscription offer endpoint gives you all of this in a single read, resolved against the plans and offers you have configured in Admin. ## Fetching a subscription offer Call `GET /store/products/:id/subscription-offer` to retrieve the effective offer for a product. Pass an optional `variant_id` query parameter to resolve the offer for a specific variant instead. ```bash theme={null} GET /store/products/:id/subscription-offer GET /store/products/:id/subscription-offer?variant_id=variant_123 ``` When `variant_id` is provided and that variant has its own offer configured, the variant-level offer takes precedence over the product-level offer. If no offer is found at either level, or if the offer is disabled, the response returns `is_subscription_available: false`. ## Response shape ```json theme={null} { "subscription_offer": { "is_subscription_available": true, "product_id": "prod_123", "variant_id": "variant_456", "source_offer_id": "offer_789", "source_scope": "variant", "allowed_frequencies": [ { "frequency_interval": "month", "frequency_value": 1, "label": "Monthly", "discount": { "type": "percentage", "value": 10 } }, { "frequency_interval": "month", "frequency_value": 3, "label": "Every 3 months", "discount": { "type": "percentage", "value": 15 } } ], "discount_semantics": "per_order", "minimum_cycles": 2, "trial": null } } ``` ## Response fields | Field | Description | | --------------------------- | ------------------------------------------------------------------------------ | | `is_subscription_available` | `true` if subscriptions are available for this product or variant | | `product_id` | The product the offer applies to | | `variant_id` | The resolved variant, if `variant_id` was passed | | `source_offer_id` | The internal ID of the offer that was resolved | | `source_scope` | Whether the offer came from `"variant"` or `"product"` level | | `allowed_frequencies` | Array of available subscription cadences (see below) | | `discount_semantics` | How the discount is applied — for example `"per_order"` | | `minimum_cycles` | The minimum number of billing cycles before cancellation is allowed, or `null` | | `trial` | Trial configuration object, or `null` if no trial is configured | ## Frequency items Each object in `allowed_frequencies` describes one selectable cadence: | Field | Description | | -------------------- | -------------------------------------------------------- | | `frequency_interval` | Time unit: `"week"`, `"month"`, or `"year"` | | `frequency_value` | Number of intervals between billings | | `label` | Human-readable label you can display directly in your UI | | `discount.type` | Discount type — for example `"percentage"` or `"fixed"` | | `discount.value` | The discount amount in the given type's unit | ## Powering a PDP subscription selector Use this endpoint to drive the subscription UI on your product detail page. A typical implementation fetches the offer on page load and uses the response to: 1. **Show or hide the subscription option** — render the subscription selector only when `is_subscription_available` is `true`. 2. **Populate the frequency selector** — render one option per item in `allowed_frequencies`, using `label` as the display text. 3. **Display the discount** — show the savings for each frequency using `discount.type` and `discount.value`. 4. **Set checkout metadata** — when the customer confirms a frequency, use the selected `frequency_interval` and `frequency_value` to set the line item metadata before calling the subscribe endpoint. ```typescript theme={null} const { subscription_offer } = await medusa.client.fetch( `/store/products/${productId}/subscription-offer?variant_id=${variantId}` ); if (!subscription_offer.is_subscription_available) { // Hide subscription option return; } // Render a frequency option for each allowed cadence const options = subscription_offer.allowed_frequencies.map((freq) => ({ label: freq.label, interval: freq.frequency_interval, value: freq.frequency_value, discount: freq.discount, })); ``` The `allowed_frequencies` array only contains cadences that are active in your Plans & Offers configuration. Use this list to validate which cadences a customer can select — do not allow customers to submit cadences that are not present in the response.