Build subscriptions
A subscription bills a customer or account on a recurring cadence. The integration shape is short: create a product, attach phases if your offer has trial or intro pricing, then create a subscription against the customer's stored payment method. Frame handles the cycle-by-cycle invoicing and charging; your application listens for the webhooks and reacts.
This guide walks through the full setup. For mid-flight changes to an active subscription, see upgrade or downgrade a subscription.
Prerequisites
| Requirement | Details |
|---|---|
| Frame secret key | For server-side product + subscription creation. |
| Customer or Account record | The payer. Pre-create via the Customers or Accounts API, or attach to the subscription create call. |
| Stored payment method | The card or bank account to charge each cycle. Collected via frame-js or attached via the PaymentMethods API. |
1. Create the product
A subscription needs a recurring product. The product carries the cadence, the default price, and any reusable phases.
curl --request POST \
--url https://api.framepayments.com/v1/products \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"name": "Pro Plan",
"description": "Monthly access to the pro tier",
"default_price": 2900,
"purchase_type": "recurring",
"recurring_interval": "monthly"
}'
Save the returned id — you'll reference it when creating subscriptions.
For multi-tier offers (monthly vs annual, or basic vs pro), create one product per tier. Don't try to model tiers within a single product — Frame's products are designed around one product = one billing shape.
2. (Optional) Attach phases for ramps or trials
If your subscription has a trial period, intro pricing, or a multi-step ramp, attach phases to the product. Each phase covers some number of billing cycles before the next phase takes over.
Free trial → paid example:
# Phase 1: free trial, one cycle
curl --request POST \
--url https://api.framepayments.com/v1/product_phases \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"product_id": "<product_id>",
"name": "Free trial",
"ordinal": 1,
"pricing_type": "static",
"amount_cents": 0,
"period_count": 1
}'
# Phase 2: paid, long-running (set period_count high for "rest of subscription")
curl --request POST \
--url https://api.framepayments.com/v1/product_phases \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"product_id": "<product_id>",
"name": "Paid",
"ordinal": 2,
"pricing_type": "static",
"amount_cents": 2900,
"period_count": 999
}'
Phases on the product are templates — they're copied onto each subscription at create time. Edits to the product's phases after creation only affect new subscriptions; existing ones keep their copied phases. See product phases for the full mechanics.
3. Create the subscription
With the product (and optional phases) in place, create a subscription:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"customer": "<customer_id>",
"product": "<product_id>",
"default_payment_method": "<payment_method_id>",
"currency": "USD",
"description": "Pro Plan — monthly",
"proration_behavior": "create_prorations"
}'
Owner choice: pass either customer or account, not both. For B2C subscriptions (individual end-user billing), use customer. For B2B (team / org billing), use account. This decision affects which webhook events fire and stays fixed for the subscription's lifetime.
The subscription is created in pending and immediately attempts activation:
- Phase 0's first charge runs against the payment method. If it's a $0 trial phase, the charge is symbolic (a $0 invoice).
- On successful first cycle,
statustransitions toactive. - On failure,
statustransitions toincomplete— fix the payment method and re-trigger.
4. Apply a discount (optional)
Subscriptions don't accept promotion_codes directly on create. Two paths to attach a discount:
Option A — discount via phase. If the discount is part of the offer ("first 3 months 50% off"), bake it into the product's phases:
curl --request POST \
--url https://api.framepayments.com/v1/product_phases \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"product_id": "<product_id>",
"name": "Intro 50% off",
"ordinal": 1,
"pricing_type": "relative",
"discount_percentage": 50,
"period_count": 3
}'
Then add a phase for the standard price at ordinal 2 (with a high period_count).
Option B — promo code at invoice time. If the discount is campaign-driven (a customer enters a code at checkout), apply it on the subscription's first invoice or via the invoice the subscription generates each cycle. The simplest path: create the invoice manually with the code, then attach a payment method to settle it.
curl --request POST \
--url https://api.framepayments.com/v1/invoices \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"customer": "<customer_id>",
"collection_method": "auto_charge",
"payment_method": "<payment_method_id>",
"promotion_codes": ["LAUNCH20"],
"line_items": [
{ "product": "<product_id>", "quantity": 1 }
]
}'
Choose by intent: phases for structural discounts ("part of the product"), promo codes for marketing-driven discounts ("part of the campaign").
5. React to webhooks
The subscription lifecycle emits webhook events your application needs to handle. Subscribe to:
For customer-owned subscriptions (B2C):
customer.subscription.activated— initial activation succeededcustomer.subscription.renewal.processing— a renewal cycle is chargingcustomer.subscription.renewal.completed— cycle settledcustomer.subscription.renewal.failed— cycle failed; needs attentioncustomer.subscription.past_due/customer.subscription.unpaid— escalating non-paymentcustomer.subscription.canceled/customer.subscription.terminated— end of life
For account-owned subscriptions (B2B), the same events fire with subscription.* prefixes (no customer. prefix). Subscribe to both prefixes if your platform supports both shapes.
// Webhook handler
switch (event.type) {
case 'customer.subscription.activated':
case 'subscription.activated':
grantAccess(event.data.customer || event.data.account);
break;
case 'customer.subscription.renewal.failed':
case 'subscription.renewal.failed':
notifyPayerToUpdatePaymentMethod(event.data);
break;
case 'customer.subscription.canceled':
case 'customer.subscription.terminated':
case 'subscription.canceled':
case 'subscription.terminated':
revokeAccess(event.data.customer || event.data.account);
break;
}
6. Handle failed renewals
When a renewal cycle's invoice fails to collect:
renewal_statusflips tofailedon the subscription.- The corresponding
*.subscription.renewal.failedwebhook fires. - The subscription itself transitions to
past_due(then escalates tounpaidif recovery doesn't happen).
Frame doesn't run dunning logic automatically — there's no built-in retry schedule for failed subscription charges. Your application owns recovery:
- Prompt the customer to update their payment method.
- Once they've updated, you can re-issue the invoice or trigger a subscription update to retry.
- If the customer abandons recovery, your platform's policy decides when to cancel.
A reasonable recovery shape:
day 0: invoice fails → notify customer
day 3: re-attempt → if still failing, escalate notice
day 7: re-attempt → if still failing, mark subscription canceled, revoke access
Schedule the retries from your application; Frame won't do it for you in V1.
7. Cancel when needed
POST /v1/subscriptions/<id>/cancel ends the subscription immediately:
curl --request POST \
--url https://api.framepayments.com/v1/subscriptions/<id>/cancel \
--header "Authorization: Bearer $FRAME_SECRET_KEY"
The subscription transitions to canceled right now — no grace period, no end-of-cycle behavior. If you want "cancel at end of current period," implement the scheduling at your application layer: store the requested cancel-at date, fire the cancel call when the period boundary hits.
Frame doesn't auto-refund the unused portion of a partially-consumed period. If your policy requires a partial refund, issue a refund against the most recent successful invoice's charge.
Common variations
B2B subscriptions. Use account instead of customer. Webhooks fire under subscription.* (no customer. prefix). Otherwise the shape is identical.
Quantity-based pricing. Set quantity on the subscription. Invoices generate with line_item.quantity × product.default_price per cycle.
Mid-cycle changes. See upgrade or downgrade a subscription for the proration mechanics.
Net-terms invoicing (B2B). Skip subscriptions entirely; create invoices manually with collection_method: request_payment and net_terms set. The customer receives the invoice and pays via the surfaced payment link.
Gotchas
Symptom: subscription stuck in incomplete after create. Why: initial charge failed. Fix: verify the payment method is valid (GET /v1/payment_methods/<id>); update if needed; trigger a re-attempt by updating the subscription's default_payment_method.
Symptom: you tried to pass promotion_codes on subscription create and it errored. Why: the subscription endpoint doesn't accept promo codes. Fix: either use phases (structural discount) or apply the promo code at invoice time (campaign discount). See step 4.
Symptom: renewal didn't fire on the day you expected. Why: renewals are cycle-driven, not wall-clock-driven. Check the subscription's current_period_end — that's when the next cycle starts. Fix: nothing to fix; it's working as intended.
Symptom: webhook event name you expected didn't fire. Why: you subscribed to customer.subscription.* but the subscription is account-owned (or vice-versa). Fix: subscribe to both prefixes, or check the subscription's owner type and route accordingly.
Next steps
- Upgrade or downgrade a subscription for mid-flight changes
- Subscriptions concept for the deeper state model
- Invoices concept for understanding the per-cycle billing object
- Coupons + promotion codes for the discount surface