Coupons
A coupon is a reusable discount template. It defines the rule — "20% off," "$10 off orders over $50," "free month for the first three billing cycles" — and lives independently of any specific customer. Customers don't redeem coupons directly; they redeem promotion codes, which wrap coupons with customer-facing strings and per-customer scoping.
The split is intentional: one coupon (the rule) can have many promotion codes (the customer-facing handles). You define SUMMER20 as a 20% coupon once and generate distinct codes for different campaigns, audiences, or customers.
Discount types
Every coupon has a discount_type:
percentage—discount_valueis a percentage (1-100). The discount applies to the charge amount.fixed_amount—discount_valueis a cents amount. A flat reduction.
For percentage coupons, the optional discount_cap_cents puts an upper bound on how much a single application can discount — useful for "20% off, max $50" promotions.
Durations
The duration field controls how a coupon behaves over time on recurring subscriptions:
once— discount applies a single time (to the first invoice or charge it touches).repeating— discount applies for a defined number of billing periods. Pair withduration_in_months(or the equivalent period field) to set the count.forever— discount applies to every cycle indefinitely.
For one-time purchases, duration is effectively always "once" since there's only one charge to apply against. For subscriptions, choose carefully — forever discounts permanently lower the customer's bill, which has long-tail revenue implications.
Fields
curl --request POST \
--url https://api.framepayments.com/v1/coupons \
--header "Authorization: Bearer $FRAME_SECRET_KEY" \
--header 'Content-Type: application/json' \
--data '{
"name": "Summer 2026 — 20% off",
"description": "Marketing campaign coupon",
"discount_type": "percentage",
"discount_value": 20,
"duration": "once",
"applicable_to": "all_products",
"max_redemptions": 1000,
"valid_from": "2026-06-01T00:00:00Z",
"valid_until": "2026-08-31T23:59:59Z",
"minimum_order_amount_cents": 5000,
"discount_cap_cents": 5000,
"is_stackable": false
}'
The interesting fields:
name— internal-facing name for dashboard navigation. Customers see the promotion code string, not this name.discount_type+discount_value— see above.duration—once/repeating/forever. Withrepeating, pair with a duration period field.applicable_to— scope of the coupon.all_products(any product) orspecific_products(restricted viaproduct_id).max_redemptions— global cap on total redemptions across all promotion codes derived from this coupon. Hits the cap → all derived codes stop accepting redemptions.valid_from/valid_until— time window. Outside this window the coupon (and all its codes) reject application.minimum_order_amount_cents— coupon only applies if the order subtotal meets this threshold.discount_cap_cents— upper bound on the discount value (relevant for percentage coupons on large orders).is_stackable— whether this coupon can be combined with other coupons on the same charge/invoice. Non-stackable coupons fail validation if other codes are also applied.
Lifecycle
Coupons carry a status state machine:
active— available for new promotion codes to derive from, and existing codes can redeem against it.inactive— paused. New codes can't redeem; existing redemptions stand.archived— kept for historical reporting but not in active use.deleted— soft-deleted. Recoverable viarestoreback toinactive.
Transitions:
active ↔ inactiveviaactivate/deactivate.active | inactive → archivedviaarchive.active | inactive | archived → deletedviadiscard.deleted → inactiveviarestore.
Stacking
When multiple promotion codes apply to the same invoice or charge, they're applied sequentially in the order supplied. Each percentage discount applies to the already-discounted amount, not the original. Two 10% codes don't equal 20% off — they equal 19% off (90% × 90% = 81%).
Plus, every coupon's is_stackable flag gates whether it can participate at all. If any coupon in the applied set has is_stackable: false, the stack fails validation.
The merchant's max_discounts ceiling sets a hard cap on how many codes can apply to a single charge regardless of stacking rules. If you need a higher ceiling than the default, contact Frame support.
Relationships
- PromotionCode —
promotion_code.coupon_idis the FK. A coupon can have many promotion codes; each code is a customer-facing redemption handle. - Product — if
applicable_to = "specific_products",coupon.product_idscopes the coupon to a specific product. - Invoice — coupons apply to invoices via promotion codes;
discount_amount_centsanddiscount_countland on the invoice record. - ChargeIntent — same pattern on the deprecated ChargeIntent surface (apply a discount at checkout).
Gotchas
Symptom: you created a coupon and tried to apply it directly to a charge — got an error. Why: coupons aren't redeemed directly. They're the template. You need to create a promotion code from the coupon, then pass the promotion code's string at checkout. Fix: POST /v1/promotion_codes with coupon_id set, then pass the resulting code string on the charge/invoice.
Symptom: forever coupon on a subscription is still discounting after you "expired" the campaign. Why: valid_until controls when new redemptions are accepted, not when existing redemptions stop applying. Once a forever coupon is attached to a subscription, it continues until the subscription itself ends or you actively detach it. Fix: if you need to end a forever discount mid-subscription, you'll need to update the subscription's pricing structure (potentially via product phases or by canceling + recreating).
Symptom: you stacked two 50% coupons and the charge wasn't free. Why: sequential application — 50% off, then 50% off the already-discounted amount = 75% off, not 100%. Fix: if you need "two 50% codes = 100% off" semantics, model it as a single 100% coupon. Sequential math is intentional; it prevents accidental free-orders from coupon stacking errors.
Symptom: coupon hits its max_redemptions and all derived codes stop working at once. Why: max_redemptions is a global counter at the coupon level, shared across all derived promotion codes. Fix: expected behavior. If you need higher capacity, raise max_redemptions on the coupon (subject to your business policy).
Reference
For the full API surface, see POST/v1/coupons and the rest of the Coupons resource.