Subscription plans

Subscription plans are reusable billing templates: they define what to charge, how often to bill, and any trial or contract terms. A plan's line items can reference your catalog (variants or bundles) or be defined ad hoc with a name and unit price, and the plan can add a setup fee, a trial period, a contract term, and an early termination fee. A plan can also offer several intervals and quantities for the buyer to choose from at signup. Every money field on a plan must use the plan's currency.

One plan can be sold through multiple channels: create subscriptions against it directly, or pass its subscription_plan_id to a checkout session or payment link for Flint-hosted signup. To sell subscriptions on any product in a cart instead, use a subscription offer. Plans are archived rather than deleted, so existing subscriptions keep billing against the plan they were created with.

Note:

Start with the Subscription billing guide. For selling plans through hosted links, see subscription signup links.

Replace plan line items#

line_items is an owned collection. Create a plan with inline items, then replace the collection with PATCH /v1/subscription-plans/{subscription_plan_id} and the plan's current expected_version.

JSON
{
  "expected_version": 3,
  "line_items": [{
    "subscription_plan_line_item_id": "spli_01J00000000000000000000000",
    "name": "Monthly support",
    "unit_price_money": {"amount": 2500, "currency": "USD"},
    "quantity": 1
  }]
}

Include an existing subscription_plan_line_item_id to retain that item's identity. Omit the ID for a new item; Flint generates it. Members omitted from the array are removed. Duplicate IDs and IDs belonging to another plan are rejected. Omit line_items to keep the collection unchanged. An empty array or null is invalid because a plan needs at least one item.

Each ad hoc item supplies its name and price. Catalog items supply variant_id or bundle_id instead. A retained catalog item with the same source keeps its recorded price, tax, display, and fulfillment snapshot; omitting modifiers retains their recorded choices. Changing the source requires an explicit modifiers array and creates a fresh catalog snapshot. Existing subscriptions keep the line-item snapshots taken when they were created.

Replacement and scalar changes in the same request commit atomically. A missing version fence returns 400; a stale version returns 409. Archived plans cannot be edited. Every item must use the plan currency and a quantity from 1 through 9999. Use the same Idempotency-Key and body to recover a lost response with the original new item IDs.

Intervals and quantities#

billing_interval_options lists up to 12 distinct pairs of billing_interval and billing_interval_count. It must include the plan's own pair, which is the default. quantity_options lists up to 10 distinct quantities from 1 to 100 and must include 1, the default. Send [] to offer only the plan's own interval or a quantity of 1. On PATCH, an omitted field keeps its value and an array replaces it; changing the plan's billing_interval to a pair the stored options don't include needs the new options in the same request. Responses always return the effective lists, with the plan's own pair first and quantities ascending. Invalid options return INVALID_BILLING_INTERVAL_OPTIONS or INVALID_QUANTITY_OPTIONS. These options apply to every plan, not only plans that ship.

The buyer chooses one interval and one quantity at signup and can change them later among the same options. A quantity multiplies every line on the plan: the subscription reports it as quantity, each subscription line keeps the plan line's per-unit quantity, and each renewal ships the two multiplied together. Multiplied, a line can carry at most 9,999 units. Saving the plan doesn't check quantity_options against that limit, so a subscription or checkout that chooses a quantity putting a line over it, or making the amount too large, fails with INVALID_QUANTITY.

Variant swaps#

A catalog line can list swap_variant_ids: up to 25 other variants of the same product that a subscriber may switch the line to after signup. [] means no swaps. Only variant lines can list swaps; bundle and ad hoc lines can't (INVALID_SWAP_VARIANTS). A subscriber can always swap back to the line's own variant_id. See Change cadence, quantity, and items.

Discounts#

Physical plans can't have a trial. Discount shipments with a promotion applied at signup: recurrence.type once covers the first shipment, repeating the first period_count charged cycles, and forever every cycle. See Discounts and first-shipment offers.

Physical products#

A catalog line whose variant or bundle is physical makes the plan ship every cycle. A catalog item with no kind counts as physical. Each renewal order then carries the subscriber's address, a shipping charge, tax for that address, and a fulfillment. See Ship physical products.

delivery_required is true when at least one line ships. subscription_delivery_method_ids lists the delivery methods subscribers can choose. Empty means the store's checkout default methods. Removing a method changes what new subscribers can choose; current subscribers keep it until it is archived or stops serving their address. delivery_method_subscription_counts reports, for each method that live subscriptions on the plan use, how many are active, paused, and past_due.

Create, line item changes, method changes, and trial changes validate the plan's physical lines:

ErrorCause
SUBSCRIPTION_DELIVERY_PROFILE_MISSINGA physical line has no delivery profile.
SUBSCRIPTION_DELIVERY_PROFILE_ACTION_REQUIREDA line's delivery profile needs an action before it can be used.
SUBSCRIPTION_FULFILLMENT_NOT_SUPPORTEDA line's profile allows only pickup. Subscriptions ship with shipment or local_delivery.
SUBSCRIPTION_DELIVERY_LINES_NOT_COMBINABLEThe physical lines would ship separately, so one method can't cover a renewal.
SUBSCRIPTION_DELIVERY_METHOD_UNSUPPORTEDNo offered method can price every shipment without the buyer present, or a listed method uses caller_supplied pricing or delivery windows.
SUBSCRIPTION_TRIAL_NOT_SUPPORTED_FOR_PHYSICALtrial_period_days is above 0. Use a promotion for a first-shipment offer instead.

Each error names what failed with fields on the error object:

  • variant_id and product_id name a variant line, or the item inside a bundle line that failed.
  • blocking_resources can list the line's variant or bundle, with resource_type variant or bundle. A bundle line is always named this way.
  • delivery_method_id names a listed method that fails the check. A method that can't be used at all, such as an archived one, is named by delivery_method_id with no line fields.

The trial and combination checks, and a method that can't serve the lines, apply to the whole plan, so their line fields name the plan's first physical line. When the error carries blocking_resources or delivery_method_id, error.details has one item with the same fields. Otherwise the error has no details.

Stock source#

inventory_routing_source says where tracked stock for the plan's orders comes from. Its type picks one of three shapes:

typeID fieldStock comes from
fixed_locationlocation_idOne Location.
policyinventory_allocation_policy_idAn allocation policy.
policy_versioninventory_allocation_policy_version_idOne immutable version of an allocation policy.
JSON
{
  "inventory_routing_source": { "type": "fixed_location", "location_id": "loc_01J00000000000000000000000" }
}

The field is required when any line item tracks inventory. Creating such a plan without it, or replacing line_items with tracked items on a plan that has no source, returns INVENTORY_ROUTING_SOURCE_REQUIRED and saves nothing. A plan whose lines don't track inventory can omit it, and a source you send anyway is validated and stored.

On PATCH, omit the field to keep the stored source. A new object replaces it as a whole, and the check runs against the plan's line items after any line_items replacement in the same request. The source can't be cleared: null returns NULL_NOT_ALLOWED on create and on update. A field from another type, or any other unknown field, returns UNKNOWN_FIELD with its full path, such as inventory_routing_source.location_id. An unknown type, a missing ID, or a malformed ID returns INVENTORY_ROUTING_SOURCE_INVALID.

Responses include inventory_routing_source when the plan has one and omit it otherwise. New subscriptions copy the plan's source when they're created, and new payment links for the plan copy it unless they send their own. Subscriptions and payment links that already exist keep the source they copied.

The Subscription plan object#

Every field on a subscription plan, as returned by retrieve and carried by the endpoints below.

Attributes

billing_intervalenumRequired
  • daily
  • weekly
  • monthly
  • yearly
billing_interval_countintegerRequired
billing_interval_optionsarray of objectRequired
contract_term_monthsinteger
created_atstring

RFC3339 timestamp.

currencystringRequired

ISO 4217 currency code.

delivery_method_subscription_countsarray of objectRequired
delivery_requiredbooleanRequired
descriptionstring
early_termination_fee_moneyobject

Monetary amount represented as integer minor units plus an ISO 4217 currency code.

external_reference_idstring

Caller-owned identifier for this resource in an external system.

imagesarray of objectRequired
inventory_routing_sourceone of

Where tracked demand from this plan is routed. New subscriptions and payment links for the plan copy it. Absent when the plan has no source.

line_itemsarray of one of
merchant_idstring
metadatamap of string
namestringRequired
quantity_optionsarray of integerRequired
setup_fee_moneyobject

Monetary amount represented as integer minor units plus an ISO 4217 currency code.

statusenumRequired
  • active
  • archived
subscription_delivery_method_idsarray of stringRequired
subscription_plan_idstringRequired
trial_period_daysinteger
updated_atstring

RFC3339 timestamp.

versionintegerRequired
JSON
{
  "billing_interval": "monthly",
  "billing_interval_count": 1,
  "billing_interval_options": [
    {
      "billing_interval": "monthly",
      "billing_interval_count": 1
    }
  ],
  "created_at": "2026-03-17T14:30:00Z",
  "currency": "USD",
  "delivery_method_subscription_counts": [],
  "delivery_required": false,
  "images": [],
  "line_items": [
    {
      "name": "Membership",
      "quantity": 1,
      "subscription_plan_line_item_id": "spli_123",
      "swap_variant_ids": [],
      "unit_price_money": {
        "amount": 2500,
        "currency": "USD"
      }
    }
  ],
  "merchant_id": "mer_123",
  "metadata": {
    "segment": "gold"
  },
  "name": "Monthly Membership",
  "quantity_options": [
    1
  ],
  "status": "active",
  "subscription_delivery_method_ids": [],
  "subscription_plan_id": "plan_123",
  "updated_at": "2026-03-17T14:30:00Z",
  "version": 1
}

List subscription plans#

GET/v1/subscription-plans

Requires scope commerce.subscription_plans.read or commerce.subscription_plans.write

Returns a paginated list of subscription plans for the authenticated merchant.

Query parameters

page_sizeinteger

Page size, default 20, max 100.

page_tokenstring

Cursor returned by the previous list response.

statusenum

Filter by subscription plan status.

  • active
  • archived
external_reference_idstring

Exact-match filter on the caller-owned external reference ID.

querystring

Search across subscription plan ID, external reference ID, name, and description. Text fields match any part of the value, and %, _ and \ are ordinary characters, not wildcards. IDs match from the start and need the type prefix, such as ord_01.

sort_byenum

Sort field.

  • name
  • created_at
  • updated_at
sort_directionenum

Sort direction.

  • asc
  • desc
created_afterstring

RFC3339 lower bound for created_at.

created_beforestring

RFC3339 upper bound for created_at.

Response · 200

dataarray of objectRequired
metaobject
next_page_tokenstring
request_idstring
curl https://api.withflintpay.com/v1/subscription-plans \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": [
    {
      "billing_interval": "monthly",
      "billing_interval_count": 1,
      "billing_interval_options": [
        {
          "billing_interval": "monthly",
          "billing_interval_count": 1
        }
      ],
      "created_at": "2026-03-17T14:30:00Z",
      "currency": "USD",
      "delivery_method_subscription_counts": [],
      "delivery_required": false,
      "images": [],
      "line_items": [
        {
          "name": "Membership",
          "quantity": 1,
          "subscription_plan_line_item_id": "spli_123",
          "swap_variant_ids": [],
          "unit_price_money": {
            "amount": 2500,
            "currency": "USD"
          }
        }
      ],
      "merchant_id": "mer_123",
      "metadata": {
        "segment": "gold"
      },
      "name": "Monthly Membership",
      "quantity_options": [
        1
      ],
      "status": "active",
      "subscription_delivery_method_ids": [],
      "subscription_plan_id": "plan_123",
      "updated_at": "2026-03-17T14:30:00Z",
      "version": 1
    }
  ],
  "next_page_token": "example",
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Create subscription plan#

POST/v1/subscription-plansIdempotent

Requires scope commerce.subscription_plans.write

Creates a subscription plan for the authenticated merchant.

Request body

billing_intervalenumRequired
  • daily
  • weekly
  • monthly
  • yearly
billing_interval_countintegerRequired
billing_interval_optionsarray of object
contract_term_monthsinteger
currencystringRequired

ISO 4217 currency code.

descriptionstring
early_termination_fee_moneyobject

Monetary amount represented as integer minor units plus an ISO 4217 currency code.

external_reference_idstring

Caller-owned identifier for this resource in an external system.

imagesarray of object

The complete desired gallery in display order. The first image is primary. Send [] to clear the gallery.

inventory_routing_sourceone of

Where tracked demand from this plan is routed: a fixed Location, an allocation policy, or an immutable policy version. Required when any line item tracks inventory. New subscriptions and payment links for the plan copy it, unless the payment link sends its own.

line_itemsarray of one of
metadatamap of string
namestringRequired
quantity_optionsarray of integer
setup_fee_moneyobject

Monetary amount represented as integer minor units plus an ISO 4217 currency code.

subscription_delivery_method_idsarray of string
trial_period_daysinteger

Response · 201

dataobjectRequired
metaobject
request_idstring
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "billing_interval": "monthly",
    "billing_interval_count": 1,
    "currency": "USD",
    "line_items": [
      {
        "name": "Membership",
        "quantity": 1,
        "unit_price_money": {
          "amount": 2500,
          "currency": "USD"
        }
      }
    ],
    "metadata": {
      "segment": "gold"
    },
    "name": "Monthly Membership"
  }'
curl https://api.withflintpay.com/v1/subscription-plans/plan_123 \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "data": {
    "billing_interval": "monthly",
    "billing_interval_count": 1,
    "billing_interval_options": [
      {
        "billing_interval": "monthly",
        "billing_interval_count": 1
      }
    ],
    "created_at": "2026-03-17T14:30:00Z",
    "currency": "USD",
    "delivery_method_subscription_counts": [],
    "delivery_required": false,
    "images": [],
    "line_items": [
      {
        "name": "Membership",
        "quantity": 1,
        "subscription_plan_line_item_id": "spli_123",
        "swap_variant_ids": [],
        "unit_price_money": {
          "amount": 2500,
          "currency": "USD"
        }
      }
    ],
    "merchant_id": "mer_123",
    "metadata": {
      "segment": "gold"
    },
    "name": "Monthly Membership",
    "quantity_options": [
      1
    ],
    "status": "active",
    "subscription_delivery_method_ids": [],
    "subscription_plan_id": "plan_123",
    "updated_at": "2026-03-17T14:30:00Z",
    "version": 1
  },
  "request_id": "bce56cba-0827-44aa-bb56-4f200ba15ee6"
}

Update subscription plan#

PATCH/v1/subscription-plans/{subscription_plan_id}Idempotent

Requires scope commerce.subscription_plans.write

Applies a sparse update to mutable subscription plan fields. Send line_items with expected_version to replace the plan's line items; omit line_items to keep them.

Path parameters

subscription_plan_idstringRequired

Flint subscription plan ID.

Request body

billing_intervalenum
  • daily
  • weekly
  • monthly
  • yearly
billing_interval_countinteger
billing_interval_optionsarray of object
contract_term_monthsinteger
descriptionstring
early_termination_fee_moneyobject

Monetary amount represented as integer minor units plus an ISO 4217 currency code.

expected_versioninteger

Resource version last read by the caller. Required when replacing an owned collection.

external_reference_idstring

Caller-owned identifier for this resource in an external system.

imagesarray of object

The complete desired gallery in display order. The first image is primary. Send [] to clear the gallery.

inventory_routing_sourceone of

Replaces the plan's routing source. Subscriptions and payment links that already exist keep the source they copied. It can't be cleared.

line_itemsarray of one of

Replaces all owned plan line items atomically. Requires expected_version. Omission leaves items unchanged; an empty array or null is invalid. Include subscription_plan_line_item_id to retain an item; omit it for a new server-generated ID. Omitted members are removed. Existing subscriptions keep their snapshots. Retained catalog items with the same source keep their catalog snapshot; omitted modifiers keep their recorded choices.

metadatamap of string or null

Caller-owned metadata. Omit this field to leave metadata unchanged. Send an object to merge by key, set a key to null to remove it, or set metadata to null to clear all metadata. An empty object makes no change. Empty strings are stored. Keys starting with flint_ are reserved and cannot be written through the public API.

namestring
quantity_optionsarray of integer
setup_fee_moneyobject

Monetary amount represented as integer minor units plus an ISO 4217 currency code.

subscription_delivery_method_idsarray of string
trial_period_daysinteger

Response · 200

Same response as Create subscription plan.

curl -X PATCH https://api.withflintpay.com/v1/subscription-plans/plan_123 \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a-unique-key" \
  -d '{
    "billing_interval_options": null,
    "description": "Updated recurring membership plan",
    "metadata": {
      "segment": "vip"
    },
    "quantity_options": null,
    "subscription_delivery_method_ids": null
  }'
curl -X DELETE https://api.withflintpay.com/v1/subscription-plans/plan_123 \
  -H "Flint-Version: 2026-09-07" \
  -H "Authorization: Bearer YOUR_API_KEY"

Was this helpful?