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 arecurring_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 → archivedviaarchiveevent. Reverse withunarchive.active | archived → deletedviadiscardevent. 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
quantityandmetadata, 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:
- Subscriptions —
subscription.product_idis 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.