Subscriptions
A subscription bills a payer on a recurring cadence against a product. The subscription captures the product, the payment method to charge, the billing cycle, and any phase-based pricing ramps. On each cycle, Frame generates an invoice, charges the payment method, and emits webhooks for the state transitions you care about.
Subscriptions are owned by either a customer or an account — never both. That distinction matters because it changes the webhook event names (see webhooks below) and reflects whether the subscription is B2C (individual customer billing) or B2B (account-level billing for teams, organizations, or platforms).
Anatomy
A subscription is a thin coordinator over several other primitives:
- Product (products) — what's being sold. Must be
purchase_type: recurring. - Phases (product phases) — pricing segments copied from the product at create time. Govern trial periods, intro pricing, ramps.
- PaymentMethod (payment methods) — the source of funds. Charged each cycle.
- Invoice (invoices) — generated per billing cycle. Carries line items, applies promo codes, settles the charge.
The subscription itself is the durable record of the agreement: who's billing, who's being billed, against what product, for how long.
Owner: customer vs account
A subscription's owner is set at create time and can't be changed:
customer_id— B2C. The subscription belongs to an individual customer; webhooks fire under thecustomer.subscription.*namespace.account_id— B2B. The subscription belongs to an account (a team, an organization, a platform user); webhooks fire under thesubscription.*namespace.
One of the two is required; both are mutually exclusive (Frame's validators reject the combo). Pick based on whether your platform models the payer as a customer or as a higher-level account.
Lifecycle (status state machine)
Subscriptions carry two AASM machines. The primary one is status:
pending(initial) — created but not yet billing. Frame transitions out of this state automatically on activation.active— billing on cadence. The normal operating state.incomplete— failed to activate (typically because the initial charge couldn't process). Distinct fromunpaid—incompletemeans "we never got off the ground."unpaid— an ongoing subscription where a cycle's invoice couldn't be collected. The subscription stays in this state until the invoice is paid or the subscription is terminated/canceled.past_due— invoice past its due date but not yet escalated tounpaid. A grace state during dunning.canceled— terminated by an explicit cancel action.terminated— ended by Frame (e.g., on permanent payment failure or end-of-life of the product).
Terminal: canceled, terminated. The other states are transient.
The standard happy path: pending → active → ... → canceled (when the customer or merchant ends it).
The interesting unhappy paths:
active → past_due → unpaid → terminated— invoice fails, dunning escalates, subscription dies.active → past_due → active— invoice fails but gets paid before escalation.pending → incomplete— initial charge failed; subscription never activated.
Renewal state machine
The secondary AASM machine is renewal_status, tracking per-renewal-cycle status independent of the broader lifecycle:
awaiting(initial) — waiting for the next billing cycle.processing— actively charging the cycle's invoice.failed— most recent renewal attempt failed.
A subscription in status: active, renewal_status: failed is one whose latest cycle didn't collect — but the subscription's broader state still says it's active. Use renewal_status as the fine-grained signal for "did this month's charge work."
Fields
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"
}'
The interesting fields:
customer/account— owner. Mutually exclusive.product— the recurring product to bill.default_payment_method— the payment method to charge on each cycle.currency— three-letter currency code.description— display name, shown on invoices.proration_behavior— what to do when the product or pricing changes mid-cycle:create_prorations(credit/charge next cycle),always_invoice(immediate proration invoice),none(no proration). See upgrade or downgrade a subscription.subscription_phases[]— optional custom phases to override the product's template phases. If omitted, Frame copies the product's phases.metadata— key-value pairs for your internal tracking.
The subscription's quantity, start_date, end_date, current_period_start, current_period_end, and billing_time are maintained by Frame as the subscription progresses.
Discounts on subscriptions
Subscriptions don't accept promotion_codes directly on create. Discounts attach via one of two paths:
- Per-invoice promo codes — apply codes on the invoices the subscription generates. Useful for cycle-specific discounts.
- Phase-based pricing — bake the discount into a
relativeorstaticphase (see product phases). Useful when the discount is structurally part of the offer ("first 3 months 50% off").
For the difference between the two, see build subscriptions.
Webhooks
Subscription state transitions fire webhooks. The event names depend on the owner type:
| Lifecycle event | Customer-owned | Account-owned |
|---|---|---|
| Activated | customer.subscription.activated | subscription.activated |
| Canceled | customer.subscription.canceled | subscription.canceled |
| Terminated | customer.subscription.terminated | subscription.terminated |
| Marked unpaid | customer.subscription.unpaid | subscription.unpaid |
| Marked past due | customer.subscription.past_due | subscription.past_due |
| Marked incomplete | customer.subscription.incomplete | subscription.incomplete |
| Renewal processing | customer.subscription.renewal.processing | subscription.renewal.processing |
| Renewal completed | customer.subscription.renewal.completed | subscription.renewal.completed |
| Renewal failed | customer.subscription.renewal.failed | subscription.renewal.failed |
Subscribe to both prefixes if your platform mixes B2C and B2B subscriptions. The payload schema is the same; only the event-code prefix differs.
Updating a subscription
PATCH /v1/subscriptions/<id> accepts changes to:
product— swap the underlying product (upgrade/downgrade). Triggers proration perproration_behavior.default_payment_method— change the card or bank account on file.description— display name.update_interval— boolean. Settruewhen swapping to a product with a differentrecurring_interval(e.g., monthly → annual); Frame uses this flag to handle the interval change explicitly rather than rejecting the update.metadata— your internal tracking fields.
For more elaborate changes (phase restructuring, mid-cycle cancellation), use the dedicated phase endpoints or the cancel endpoint.
Canceling
POST /v1/subscriptions/<id>/cancel transitions an active subscription to canceled immediately. No grace period, no end-of-cycle behavior — the cancel is right now. If you need "cancel at period end," implement that at your application layer: schedule the cancel call for the period boundary.
Gotchas
Symptom: you created a subscription with promotion_codes and it errored. Why: the subscription create endpoint doesn't accept promotion_codes — discounts attach via invoices or phases. Fix: apply the promo code at invoice time (or bake it into a phase). See build subscriptions for the patterns.
Symptom: the webhook event name you expected doesn't fire. Why: the prefix changes based on owner type. If your subscription is account-owned, listen for subscription.activated, not customer.subscription.activated. Fix: subscribe to both prefixes, or inspect each subscription's owner field to know which to expect.
Symptom: a relative phase on the product doesn't apply the discount the customer expected. Why: relative phases discount off the product's current default_price. If the price has moved since the subscription was created, the discount amount has moved too. Fix: use a static phase for fixed-dollar guarantees.
Symptom: subscription stuck in incomplete. Why: the initial charge couldn't process — most often a payment method issue. Fix: update the subscription's default_payment_method and re-trigger the initial charge. If the payment method is fine and the failure was transient, contact Frame support.
Symptom: cancel left the customer with an unbilled partial period and no refund. Why: cancel is immediate, and Frame doesn't auto-prorate refunds on cancellation. Fix: if your policy requires a refund for the unused portion, issue a refund against the most recent successful invoice's charge.
Reference
For the full API surface, see POST/v1/subscriptions and the rest of the Subscriptions resource.