Coupons
Coupons are discount campaigns redeemed at checkout. They come in two kinds:
generated— codes are minted in batches viaPOST /coupons/{id}/codes. Each code is typically unique per customer.promo— a single shared code whose value equals the coupon's normalized name (e.g.BLACKFRIDAY). The code is auto-created at coupon-create time;POST /coupons/{id}/codesis rejected with422for these coupons.
To retrieve the code value for either kind, call GET /v1/coupons/{id}/codes — for promo, the list contains the single auto-minted row; for generated, the full minted batch.
To apply a coupon at checkout, send the code as coupon_code on POST /v1/orders (one-time purchase) or on POST /v1/subscriptions (recurring — the discount is applied to the first cycle and persists through later cycles per the coupon's duration). Renewal billing orders themselves are server-generated and do not accept a coupon_code; downstream cycles read the snapshot captured at the first redemption.
Discount Terms
A coupon is either percentage-off (percentage, 1–100) or amount-off (amount in cents) — exactly one of the two must be set.
max_discount_amountcaps the discount a percentage coupon can apply per redemption (in cents). Rejected on amount-off coupons (the fixed amount already plays the role of a cap). Cap value is snapshotted at redemption time — subsequent edits to the live coupon do not retroactively change historical orders.currencyis part of the redemption snapshot for amount-off coupons. Percentage coupons are currency-agnostic; sending a non-USDcurrency for a percentage coupon is rejected.
Duration
Subscription semantics — how many billing cycles the discount applies for:
once— the discount applies to a single charge.repeating— the discount applies forduration_in_cyclesconsecutive cycles (required whenrepeating).forever— the discount applies to every cycle for the life of the subscription.
Immutability After First Redemption
Once a coupon has been redeemed at least once, certain fields become immutable — changing them would retroactively alter what existing redeemers were promised. PATCH /coupons/{id} rejects locked fields with the field_locked error code.
Locked on first redemption: percentage, amount, max_discount_amount, currency, duration, duration_in_cycles, first_time_customer_only, max_redemptions_per_code, product_scope, plan_scope, plan_ids, product_ids. Promo coupons additionally lock name (because the name is the code). starts_at locks once the activation moment has passed.
Always editable: name (generated coupons), description, active, expires_at, minimum_amount, max_redemptions, max_redemptions_per_customer. max_redemptions cannot be set below the current redemption count.
Validation (Hosted Checkout)
POST /coupons/validate previews whether a code is redeemable for a given plan/product/customer without consuming a redemption. The final redemption happens transactionally as part of order/subscription creation. Inputs (code, customer ID, cart amount) travel in the JSON body rather than the query string so they don't end up in proxy or access logs.
Permissions
| Endpoint | Required permission |
|---|---|
POST /v1/coupons, PATCH /v1/coupons/{id}, DELETE /v1/coupons/{id}, POST /v1/coupons/{id}/archive, POST /v1/coupons/{id}/codes |
coupons:write |
GET /v1/coupons, GET /v1/coupons/{id}, GET /v1/coupons/{id}/codes, POST /v1/coupons/validate |
coupons:read |
Existing API keys do not automatically gain coupon access — rotate or update the key with the new permission to use these endpoints.
/coupons Create a coupon
Creates a new coupon. Requires the coupons:write permission and an idempotency key.
Coupon Kinds
kind: generated(default) — no codes are minted at create time. CallPOST /coupons/{id}/codeslater, or pass acodesblock on this request to mint a random batch inline.kind: promo— a single shared code is auto-created using the trimmed/uppercasednameas its value.POST /coupons/{id}/codesis rejected for promo coupons.
Discount Shape
Exactly one of percentage or amount must be provided. max_discount_amount is percentage-only and is rejected when paired with amount.
Idempotency
Requires an X-EPD-Idempotency-Key header. Replaying the same key within 24 hours returns the original response without creating a duplicate coupon.
Header parameters
string"2026-02-11"string (uuid)"550e8400-e29b-41d4-a716-446655440000"Request body required
enumgeneratedpromostring"BLACKFRIDAY2026"string"15% off first order — use in welcome email."number15integer1000stringenumoncerepeatingforeverintegerintegerintegerbooleanintegerintegerintegerstring (date-time)string (date-time)enumnoneallspecificenumnoneallspecificarray[string]array[string]objectintegerstring"SUMMER"integerstring (date-time)Responses
array[CouponCode]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons List coupons
Returns a paginated list of coupons. Archived coupons are hidden by default — use archived=true to return only archived, or archived=all to include both.
Query parameters
integer10string"550e8400-e29b-41d4-a716-446655440000"string"550e8400-e29b-41d4-a716-446655440001"booleanenumgeneratedpromoenumtruefalseallstring"created_at[desc]"Header parameters
string"2026-02-11"Responses
array[Coupon]any"/v1/coupons"objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/validate Validate a coupon code
Previews whether a code is redeemable in the supplied context. Does not consume a redemption — the final redemption happens transactionally inside POST /v1/orders or POST /v1/subscriptions when you pass coupon_code. Renewal billing orders themselves are server-generated and do not accept a coupon_code; downstream subscription cycles read the snapshot captured at the first redemption.
Response
Returns either { valid: true, ... } with discount terms (including max_discount_amount so callers can preview the discount), or { valid: false, reason, code } with a machine-readable ineligibility reason.
Computing the Preview Discount
When valid: true, compute the line-item discount client-side from the returned terms:
- Amount-off:
discount = min(amount, cart_total)(in cents). - Percentage-off:
discount = floor(cart_total * percentage / 100), then clamp withmax_discount_amountif non-null:discount = min(discount, max_discount_amount).
Example: a 15% coupon with max_discount_amount: 2500 against a $200 cart returns discount = min(floor(20000 * 15 / 100), 2500) = min(3000, 2500) = 2500 cents — i.e. $25 off, capped.
Server-side redemption inside POST /v1/orders uses the same math, so this preview matches what the customer is actually charged.
Always 200 for Ineligibility
Unknown codes return 200 with valid: false, reason: "code_not_found" rather than 404. Inactive, expired, exhausted, currency-mismatched, and scope-ineligible failures all flow through the same 200 envelope so callers can render a single error path. Non-200 responses are reserved for transport or auth failures (400, 401/403, 429).
Why a JSON Body
Inputs travel in the request body so customer IDs and cart amounts don't land in URL parameters that proxies and access logs retain.
Throttling
Subject to the standard per-merchant rate limit (429 too_many_requests with the usual Retry-After, RateLimit-* headers). Debounce in your checkout UI — call this on onBlur or after a 300 ms input pause, not on every keystroke.
Header parameters
string"2026-02-11"Request body required
string"BLKFRDY-A7K9MZQ2"stringstringstringintegerstringResponses
objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/{id} Retrieve a coupon
Path parameters
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"Header parameters
string"2026-02-11"Responses
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"string"BLACKFRIDAY2026"string"15% off first order — use in welcome email."enumgeneratedpromo"generated"number15integernullstring"usd"enumoncerepeatingforever"once"integernullinteger5000integer2500booleanfalseinteger1000integer1integer1string (date-time)"2026-11-25T00:00:00.000Z"string (date-time)"2026-12-01T23:59:59.000Z"booleantruestring (date-time)nullenumnoneallspecific"all"enumnoneallspecific"specific"array[string][]array[string][]integer42string"SUMMER-"integer12string (date-time)"2026-11-01T10:30:00.000Z"string (date-time)"2026-11-25T00:00:01.000Z"objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/{id} Update a coupon
Partial update — only the fields you send change.
Immutability After First Redemption
Once a coupon has been redeemed at least once, discount terms (percentage, amount, currency, duration, duration_in_cycles, max_discount_amount), eligibility flags (first_time_customer_only, max_redemptions_per_code), and scope (product_scope, plan_scope, plan_ids, product_ids) lock. Editing a locked field returns 422 with code field_locked. name (on generated coupons), active, description, expires_at, minimum_amount, max_redemptions, and max_redemptions_per_customer remain editable. starts_at locks once the activation moment has passed.
Path parameters
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"Header parameters
string"2026-02-11"string (uuid)"550e8400-e29b-41d4-a716-446655440000"Request body required
stringbooleanstringnumberintegerstringenumoncerepeatingforeverintegerintegerintegerbooleanintegerintegerintegerstring (date-time)string (date-time)enumnoneallspecificenumnoneallspecificarray[string]array[string]Responses
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"string"BLACKFRIDAY2026"string"15% off first order — use in welcome email."enumgeneratedpromo"generated"number15integernullstring"usd"enumoncerepeatingforever"once"integernullinteger5000integer2500booleanfalseinteger1000integer1integer1string (date-time)"2026-11-25T00:00:00.000Z"string (date-time)"2026-12-01T23:59:59.000Z"booleantruestring (date-time)nullenumnoneallspecific"all"enumnoneallspecific"specific"array[string][]array[string][]integer42string"SUMMER-"integer12string (date-time)"2026-11-01T10:30:00.000Z"string (date-time)"2026-11-25T00:00:01.000Z"objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/{id} Archive a coupon
Soft-archives a coupon. Coupons are never hard-deleted — DELETE is an alias for POST /coupons/{id}/archive with { "archived": true }. Redemption history is preserved. To restore, call POST /coupons/{id}/archive with { "archived": false }. Idempotent.
Path parameters
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"Header parameters
string"2026-02-11"string (uuid)"550e8400-e29b-41d4-a716-446655440000"Responses
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"string"BLACKFRIDAY2026"string"15% off first order — use in welcome email."enumgeneratedpromo"generated"number15integernullstring"usd"enumoncerepeatingforever"once"integernullinteger5000integer2500booleanfalseinteger1000integer1integer1string (date-time)"2026-11-25T00:00:00.000Z"string (date-time)"2026-12-01T23:59:59.000Z"booleantruestring (date-time)nullenumnoneallspecific"all"enumnoneallspecific"specific"array[string][]array[string][]integer42string"SUMMER-"integer12string (date-time)"2026-11-01T10:30:00.000Z"string (date-time)"2026-11-25T00:00:01.000Z"objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/{id}/archive Archive or unarchive a coupon
Soft-archive ({ "archived": true }) or restore ({ "archived": false }). Redemption history is preserved across both transitions. Idempotent.
Note on unarchive: archiving sets active: false so paused redemptions match the merchant's intent. Unarchiving only clears archived_at — it does not flip active back to true. The coupon returns to the default list but stays paused until you call PATCH /v1/coupons/{id} with { "active": true }.
Path parameters
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"Header parameters
string"2026-02-11"string (uuid)"550e8400-e29b-41d4-a716-446655440000"Request body required
booleanResponses
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"string"BLACKFRIDAY2026"string"15% off first order — use in welcome email."enumgeneratedpromo"generated"number15integernullstring"usd"enumoncerepeatingforever"once"integernullinteger5000integer2500booleanfalseinteger1000integer1integer1string (date-time)"2026-11-25T00:00:00.000Z"string (date-time)"2026-12-01T23:59:59.000Z"booleantruestring (date-time)nullenumnoneallspecific"all"enumnoneallspecific"specific"array[string][]array[string][]integer42string"SUMMER-"integer12string (date-time)"2026-11-01T10:30:00.000Z"string (date-time)"2026-11-25T00:00:01.000Z"objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/{id}/codes Mint codes for a coupon
Mints codes for a generated coupon.
- Provide exactly one of
count(random codes) orcodes(caller-supplied literals matching^[A-Z0-9-]{8,50}$). - Random suffixes use an unambiguous alphabet.
- Up to 500 codes per call.
- Rejected with
422forkind: promo(those mint a single shared code at create time).
Idempotency
Requires an X-EPD-Idempotency-Key header. Replays with the same key return the originally-minted batch.
Path parameters
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"Header parameters
string"2026-02-11"string (uuid)"550e8400-e29b-41d4-a716-446655440000"Request body required
integer100array[string]string"SUMMER"integerstring (date-time)Responses
objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]/coupons/{id}/codes List codes for a coupon
Path parameters
string (uuid)"6ba7b820-9dad-11d1-80b4-00c04fd430c8"Query parameters
integer10string"550e8400-e29b-41d4-a716-446655440000"string"550e8400-e29b-41d4-a716-446655440001"booleanstring"created_at[desc]"Header parameters
string"2026-02-11"Responses
array[CouponCode]any"/v1/coupons/{id}/codes"objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]objectenuminvalid_request_errorauthentication_errorauthorization_errorrate_limit_erroridempotency_errorprocessing_errorwebhook_errorstring"validation_error"string"Request validation failed"string"email"string"req_a1b2c3d4e5f67890abcdef0123456789"array[object]