Validate Coupon Code
const url = 'https://example.com/api/billing/coupons/validate';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"billing_cycle":"monthly","code":"example","plan_code":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/api/billing/coupons/validate \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --data '{ "billing_cycle": "monthly", "code": "example", "plan_code": "example" }'Advisory “is this code good, and at what price?” — always 200.
Invalidity is data, not an HTTP error (§6.1): the console renders the reason string inline next to the input, and a 4xx would make every keystroke look like a failed request. Read-only — no redemption slot is consumed, so a debounced field can call this freely.
No org scope and no billing-owner gate: the answer is derived entirely
from the public plan catalog and a code the caller already typed, so
requiring an org would only stop a signed-in user from pricing an add-on
before they have one. An unknown plan_code is a bad request rather than
a coupon verdict, hence 422 (see coupons.UnknownPlanError).
Rate-limited per user (burst + daily) because the same properties that make it advisory make it an oracle: a valid code comes back with its discount_type and discount_value, so an unthrottled signed-in caller could sweep the coupon namespace and harvest every live code. See app.core.rate_limit’s coupon-validate block for why the buckets are keyed on the user rather than on the IP.
Authorizations
Section titled “ Authorizations ”Request Bodyrequired
Section titled “ Request Bodyrequired ”POST /api/billing/coupons/validate.
object
Responses
Section titled “ Responses ”Successful Response
POST /api/billing/coupons/validate — always 200; validity is data.
reason carries the §6.1 wire-level rejection strings verbatim
(CouponRejectionReason values, e.g. “coupon_expired”) — the console maps
them to copy, so they are API surface.
object
Example generated
{ "discount_type": "example", "discount_value": 1, "final_unit_price_cents": 1, "reason": "example", "valid": true}Validation Error
object
object
object
Example generated
{ "detail": [ { "ctx": {}, "input": "example", "loc": [ "example" ], "msg": "example", "type": "example" } ]}