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_intervalis only read whenpurchase_typeis"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
- Subscription lifecycle — states, renewals, and how pausing is reported.
- Receive webhooks — the general webhook receiver pattern.