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 the customer.subscription.* namespace.
  • account_id — B2B. The subscription belongs to an account (a team, an organization, a platform user); webhooks fire under the subscription.* 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 from unpaidincomplete means "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 to unpaid. 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 relative or static phase (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 eventCustomer-ownedAccount-owned
Activatedcustomer.subscription.activatedsubscription.activated
Canceledcustomer.subscription.canceledsubscription.canceled
Terminatedcustomer.subscription.terminatedsubscription.terminated
Marked unpaidcustomer.subscription.unpaidsubscription.unpaid
Marked past duecustomer.subscription.past_duesubscription.past_due
Marked incompletecustomer.subscription.incompletesubscription.incomplete
Renewal processingcustomer.subscription.renewal.processingsubscription.renewal.processing
Renewal completedcustomer.subscription.renewal.completedsubscription.renewal.completed
Renewal failedcustomer.subscription.renewal.failedsubscription.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 per proration_behavior.
  • default_payment_method — change the card or bank account on file.
  • description — display name.
  • update_interval — boolean. Set true when swapping to a product with a different recurring_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.