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

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

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

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

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

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

<ParamField query="q" type="string">
  Free-text search across name and product/variant titles.
</ParamField>

<ParamField query="order" type="string">
  Field to sort by. Database-backed: `name`, `scope`, `is_enabled`, `created_at`, `updated_at`. In-memory: `status`, `product_title`, `variant_title`.
</ParamField>

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

<ParamField query="is_enabled" type="boolean">
  Filter by enabled/disabled state.
</ParamField>

<ParamField query="scope" type="string">
  Filter by target scope. One of `product` or `variant`.
</ParamField>

<ParamField query="product_id" type="string">
  Filter by product ID.
</ParamField>

<ParamField query="variant_id" type="string">
  Filter by variant ID.
</ParamField>

<ParamField query="frequency" type="string">
  Filter by frequency interval. One of `week`, `month`, or `year`.
</ParamField>

<ParamField query="discount_min" type="number">
  Minimum discount value filter.
</ParamField>

<ParamField query="discount_max" type="number">
  Maximum discount value filter.
</ParamField>

### Response

<ResponseField name="plan_offers" type="object[]" required>
  Array of plan offer list items.

  <Expandable title="plan offer properties">
    <ResponseField name="id" type="string">Plan offer ID.</ResponseField>
    <ResponseField name="name" type="string">Offer name.</ResponseField>
    <ResponseField name="status" type="string">Human-readable status label.</ResponseField>
    <ResponseField name="is_enabled" type="boolean">Whether the offer is enabled.</ResponseField>
    <ResponseField name="target" type="object">Scoped target with `scope`, `product_id`, `product_title`, and optionally `variant_id`, `variant_title`, `sku`.</ResponseField>
    <ResponseField name="allowed_frequencies" type="object[]">Array of allowed cadences with `interval`, `value`, and `label`.</ResponseField>
    <ResponseField name="discounts" type="object[]">Array of per-frequency discount definitions.</ResponseField>
    <ResponseField name="rules_summary" type="string">Human-readable rules summary.</ResponseField>
    <ResponseField name="effective_config_summary" type="object">Resolved effective configuration including source scope, allowed frequencies, discounts, and rules.</ResponseField>
    <ResponseField name="updated_at" type="string">Last update timestamp.</ResponseField>
  </Expandable>
</ResponseField>

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

```json Response example theme={null}
{
  "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

<ParamField path="id" type="string" required>
  Plan offer ID.
</ParamField>

### Response

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

<ResponseField name="plan_offer" type="object" required>
  <Expandable title="additional detail fields">
    <ResponseField name="rules" type="object">Full rules object with `minimum_cycles`, `trial_enabled`, `trial_days`, and `stacking_policy`.</ResponseField>
    <ResponseField name="metadata" type="object | null">Arbitrary metadata.</ResponseField>
    <ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>
  </Expandable>
</ResponseField>

```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

<ParamField body="name" type="string" required>
  Offer name. Trimmed.
</ParamField>

<ParamField body="scope" type="string" required>
  Target scope. One of `product` or `variant`.
</ParamField>

<ParamField body="product_id" type="string" required>
  Target product ID.
</ParamField>

<ParamField body="variant_id" type="string">
  Target variant ID. Required for `variant`-scoped offers; must be omitted or `null` for `product`-scoped offers.
</ParamField>

<ParamField body="is_enabled" type="boolean" required>
  Whether the offer is active.
</ParamField>

<ParamField body="allowed_frequencies" type="object[]" required>
  Array of allowed cadences. Each entry requires `interval` (`week`, `month`, or `year`) and a positive integer `value`. Must contain at least one entry.
</ParamField>

<ParamField body="discounts" type="object[]">
  Per-frequency discount definitions. Each entry requires `interval`, `frequency_value`, `type` (`percentage` or `fixed`), and `value`. Optional.
</ParamField>

<ParamField body="rules" type="object">
  Rules object with `minimum_cycles`, `trial_enabled`, `trial_days`, and `stacking_policy`. Optional.
</ParamField>

<ParamField body="metadata" type="object">
  Arbitrary metadata. Optional.
</ParamField>

```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

<ParamField path="id" type="string" required>
  Plan offer ID.
</ParamField>

### Body parameters

All fields are optional; omit any field you do not want to change.

<ParamField body="name" type="string">
  Updated offer name.
</ParamField>

<ParamField body="is_enabled" type="boolean">
  Updated enabled state.
</ParamField>

<ParamField body="allowed_frequencies" type="object[]">
  Replacement list of allowed cadences.
</ParamField>

<ParamField body="discounts" type="object[]">
  Replacement list of per-frequency discounts.
</ParamField>

<ParamField body="rules" type="object">
  Updated rules object.
</ParamField>

<ParamField body="metadata" type="object">
  Updated metadata.
</ParamField>

```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

<ParamField path="id" type="string" required>
  Plan offer ID.
</ParamField>

### Body parameters

<ParamField body="is_enabled" type="boolean" required>
  Target enabled state.
</ParamField>

```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 |
