Products

A Product is the catalog entry for something a merchant sells on Frame. It can be a one-time purchase (a t-shirt, a digital download) or a recurring subscription (a monthly plan, an annual seat license). Downstream primitives — Subscriptions, Invoices, PaymentLinks — reference Products by ID and inherit their pricing and configuration.

Products are merchant-scoped: each merchant has their own catalog. There's no platform-wide product list.

One-time vs recurring

Every product has a purchase_type set at creation:

  • one_time — a single charge per purchase. Used by invoices, payment links, and ad-hoc checkout flows.
  • recurring — a subscription product. Requires a recurring_interval (daily, weekly, monthly, every_3_months, every_6_months, yearly). Used by Subscriptions.

Recurring products must carry a recurring_interval; one-time products must not. This is enforced at the API layer — if you create a recurring product without an interval, the request fails validation. If you need to sell the same underlying offering as both one-time and recurring (a course you offer either as a single purchase or a monthly cohort), create two Products.

A third billing type exists on the model (Product::PurchaseTypes) for platform-internal Frame billing — it's not part of the merchant-facing surface and shouldn't be set on products you create.

Fields

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",
    "shippable": false
  }'

The interesting fields:

  • name — display name shown in checkout, invoices, dashboard.
  • description — optional long-form description.
  • default_price — the price in cents that subscriptions and invoices default to when they reference this product. Can be overridden at the line-item or subscription-phase level.
  • purchase_type + recurring_interval — see above.
  • shippable — boolean, used by checkout flows that need to collect shipping addresses.
  • billing_credits — optional integer credit allocation tied to the product (relevant for products that bundle prepaid credits — see billing credits).
  • product_image — optional ActiveStorage-backed image for use in payment links and hosted checkout.

Lifecycle

Products carry a status state machine:

  • active — sellable. Default state.
  • archived — kept for historical reference but no longer offered. Subscriptions and invoices already attached to an archived product continue working; new uses get blocked at the application layer (your platform may also enforce this).
  • deleted — soft-deleted. The product is removed from the catalog and its image (if any) is purged.

Transitions:

  • active → archived via archive event. Reverse with unarchive.
  • active | archived → deleted via discard event. Terminal.

Archive instead of deleting whenever you can — deleted products lose their image and are harder to recover for historical reporting.

Pricing customization

The product's default_price is the headline number. Most flows let you override it case-by-case:

  • Subscriptions: subscription phases (see product phases) can override price per phase — typical for trial periods, intro pricing, or annual-vs-monthly tiering.
  • Invoices: invoice line items inherit pricing from their referenced product — they take quantity and metadata, but not a per-line price override. To bill a different amount, model it as a separate product or apply a discount via promotion codes.
  • Payment links: link sessions reference products but pricing is governed by the product's default at session-create time.

If you find yourself wanting a different price for a one-off charge, that's a signal you should model the variant as a separate product or use product phases.

Relationships

A product fans out into several downstream primitives:

  • Subscriptionssubscription.product_id is the FK. A subscription captures the product's pricing at create time so future product price changes don't retroactively mutate active subscriptions.
  • Invoice line items — each line references a product (optionally; lines can also carry inline descriptions without a product reference for one-off charges).
  • Product phases — phases attach polymorphically; a phase on a Product is a reusable template, a phase on a Subscription is instance-specific.
  • Coupons — a coupon can be scoped to a specific product via coupon.product_id. When scoped, the coupon only applies to charges/invoices for that product.

Gotchas

Symptom: you can't create a recurring product without recurring_interval. Why: the model rejects recurring products without a defined billing cadence. Fix: supply recurring_interval (monthly, etc.) or change purchase_type to one_time.

Symptom: you updated a product's default_price and existing subscriptions still bill the old amount. Why: subscriptions capture price at creation time so customer-facing prices stay stable. New subscriptions will use the updated default. Fix: if you want existing subscriptions to migrate to a new price, update them individually (see upgrade or downgrade a subscription).

Symptom: you archived a product but it's still showing up in customer-facing flows. Why: archived blocks the product from new uses, but doesn't disable existing subscriptions or pending payment links that already reference it. Fix: if you need to retire a product immediately, archive it AND audit downstream references (subscriptions, payment links, draft invoices).

Symptom: you deleted a product and now historical invoices show a broken reference. Why: invoices retain the product FK; deleted products may render as a placeholder. Fix: archive instead of delete unless you're sure no historical references matter. Once deleted, the product is gone.

Reference

For the full API surface, see POST/v1/products and the rest of the Products resource.