TutorialsBuild a subscription checkout

Build a subscription checkout

Start a recurring subscription the same way you start a one-time order — one call to the checkout sessions endpoint, with a couple of extra fields on the item. This page covers the differences from Cart to checkout to order; read that first if you haven’t already. For how a subscription behaves after it’s created — states, pausing, renewals — see Subscription lifecycle.

1. Create a subscription checkout session

Add purchase_type: "subscription" to an item, and give it a variant_id — a subscription line always needs a specific variant, unlike a one-time line which can be product-level:

curl -X POST https://api-test.theorchard.dev/api/v1/checkout/sessions \
  -H "Authorization: Bearer sk_test_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "product_slug": "tea-club",
        "variant_id": "0191...vsub",
        "quantity": 1,
        "purchase_type": "subscription",
        "subscription_interval": "month"
      }
    ],
    "return_url": "https://your-store.com/checkout/success",
    "cancel_url": "https://your-store.com/cart",
    "customer_email": "shopper@example.com"
  }'
  • purchase_type — omit it (or set "one_time") for a normal order. subscription_interval is only read when purchase_type is "subscription".
  • subscription_interval"month" or "quarter". Quarterly bills every three months; Stripe has no native quarterly interval, so Orchard maps it to a 3-month interval count under the hood.
  • A cart can mix one-time and subscription lines. The subscription line drives the recurring charge; any one-time lines in the same cart are billed once, on the first invoice, alongside it.

The response is the same shape as a one-time checkout:

{
  "checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
  "order_id": "0191...o9",
  "order_number": "ORD-0001"
}

Redirect the shopper to checkout_url exactly as you would for a one-time order.

2. Duplicate subscriptions are blocked

If the customer (matched by customer_email) already holds a live subscription for a product in the cart, the request is rejected before Stripe is ever involved:

{
  "error": "duplicate_subscription",
  "detail": "This customer already has a live subscription for a product in the cart. Manage the existing subscription instead of creating a second one.",
  "conflicts": [
    { "product_id": "0191...p1", "product_name": "Tea Club", "stripe_subscription_id": "sub_..." }
  ]
}

Status 409. “Live” means active, trialing, past_due, or paused — a fully canceled subscription doesn’t block a new one. This check only runs when customer_email is present; a guest checkout has no identity to check against and passes through. Handle the 409 by pointing the shopper at managing their existing subscription instead of retrying the same request.

3. Reconciling with your own records

Every Stripe line item this endpoint creates — subscription or one-time — carries orchard_product_id and (when the cart item had one) orchard_variant_id in its product_data.metadata. Use these to map a Stripe line item back to your catalog without matching on product name.

Repeat subscribers are also matched to their existing Stripe customer record by customer_email automatically — you don’t need to do anything to avoid minting a duplicate Stripe customer on a second subscription purchase.

4. React to subscription events

Subscribe to the subscription.created, subscription.updated, and subscription.canceled webhooks to track state changes — don’t poll. Treat each delivery as the current snapshot rather than an ordered event; see Verifying signatures for the same verification steps used for order webhooks.

Next