Coupon Tools
A coupon is one code many buyers can redeem for money off at checkout, the multi-use counterpart of an access code.
A coupon is one code many buyers can redeem for money off at checkout, the multi-use counterpart of an access code. These tools create coupons, switch them on and off, list redemptions and retire them.
Tools
activate_couponActivate CouponDESTRUCTIVEcreate_couponCreate CouponDESTRUCTIVEdeactivate_couponDeactivate CouponDESTRUCTIVEdelete_couponDelete CouponREADget_couponGet CouponREADlist_couponsList CouponsDESTRUCTIVEupdate_couponUpdate Couponactivate_coupon
DESTRUCTIVESwitch a coupon code on so subscribers can redeem it again, subject to its own dates and cap. Emits coupon.activated.
Turn a coupon back on. Switching on does not override the code's own rules: it still honours its
starts_at, expires_at and redemption cap, so an activated code can come back with
redeemable: false when it is outside its window or exhausted. Read that field, not active, to
tell a subscriber whether the code will work.
This is the counterpart of deactivate_coupon and the narrow
alternative to update_coupon with active: true: it announces
coupon.activated rather than a generic coupon.updated, so an
automation that cares about codes going live does not have to diff payloads to notice.
Entitlement is checked on the way back on
Requires the Coupons Addon or a Growth plan on the project owner. A
creator whose tier lost coupons is told why the code stays off with
TEAM_TIER_REQUIRED; the code is left as it was.
Re-calling on a code that is already on leaves it unchanged. The event emits on every successful call, so key a consumer on the coupon's state rather than on counting events.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires one event
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.
Arguments
coupon_id*stringUUID of the coupon to switch on.
What it returns
{ "data": { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "code": "BLACKFRIDAY", "name": "Black Friday 2026", "discount_type": "percentage", "discount_value": "25.0000", "currency": null, "duration": "once", "max_redemptions": 500, "max_redemptions_per_user": 1, "minimum_amount": null, "redemptions": { "count": 137, "remaining": 363, "exhausted": false }, "starts_at": "2026-11-27T00:00:00+00:00", "expires_at": "2026-12-01T00:00:00+00:00", "plan_ids": [], "active": true, "redeemable": true, "created_at": "2026-11-20T09:14:02+00:00" }}The full get_coupon row, with active: true. redeemable is the field
that says whether the code will apply at checkout right now.
How it fails
TEAM_TIER_REQUIREDthe project owner's tier no longer includes coupons. error.context.reason
VALIDATION_FAILEDthe caller is a team member whose role lacks the team's coupon-update
RESOURCE_NOT_FOUNDunknown coupon_id, a coupon on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:update.
create_coupon
DESTRUCTIVECreate a coupon code any number of subscribers can redeem for money off. Returns the coupon immediately — no job to poll.
Author a coupon code: one string many subscribers redeem for a discount at checkout. Distinct from an access code, which is one code for one person and grants access outright without a payment.
Synchronous — the coupon is returned in the response, and
coupon.created emits once.
Discounts apply to the first payment only. Renewals charge list price, so an agent should not describe a coupon as changing a subscriber's ongoing rate.
Requires the Coupons Addon or a Growth plan. Without it the call fails
with TEAM_TIER_REQUIRED rather than creating anything. Entitlement is
checked again when a subscriber redeems, so a coupon created today stops
applying if the addon later lapses.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires one event
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. It reaches beyond Subscriby: a connector, a provider or a member.
Arguments
project_id*stringUUID of the project the code belongs to.
code*stringThe code subscribers type at checkout. Letters, numbers and dashes only, 3 to 64 characters. Stored uppercase.
namestringoptionalInternal label to tell your codes apart, e.g. "Black Friday 2026". Defaults to the code itself.
discount_type*stringEither "percentage" (works on plans in any currency) or "fixed" (requires currency_id, and only applies to plans priced in it).
discount_value*numberFor percentage, 1..99. For fixed, an amount greater than zero in the chosen currency.
currency_idstringoptionalCurrency UUID. Required when discount_type is "fixed", ignored otherwise.
max_redemptionsintegeroptionalTotal uses allowed across everyone. Omit for unlimited.
1max_redemptions_per_userintegeroptionalHow many times one subscriber may use the code. Defaults to 1.
1minimum_amountnumberoptionalOnly apply the code when the plan costs at least this much, in the plan's own currency.
starts_atstringoptionalISO 8601 instant the code becomes usable. Omit to start immediately.
expires_atstringoptionalISO 8601 instant the code stops working. Omit for no end date.
plan_idsstring[]optionalPlan UUIDs to restrict the code to. Leave empty or omit to cover every plan in the project, including ones added later.
activebooleanoptionalWhether the code is usable right away. Defaults to true.
What it returns
{ "data": { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "code": "BLACKFRIDAY", "name": "Black Friday 2026", "discount_type": "percentage", "discount_value": "25.0000", "currency": null, "duration": "once", "max_redemptions": 500, "max_redemptions_per_user": 1, "minimum_amount": null, "starts_at": "2026-11-27T00:00:00+00:00", "expires_at": "2026-12-01T00:00:00+00:00", "plan_ids": [], "active": true, "redeemable": true, "created_at": "2026-11-20T09:14:02+00:00" }}code comes back uppercase regardless of what was sent. discount_value is a decimal string —
parse it as a decimal, not a float.
How it fails
TEAM_TIER_REQUIREDthe creator has neither the Coupons Addon nor a Growth plan.
VALIDATION_FAILEDcode already taken in this project (compared uppercase), code outside 3–64
RESOURCE_NOT_FOUNDunknown project_id, or the project is outside the token's scope.
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:create.
deactivate_coupon
DESTRUCTIVESwitch a coupon code off so no new subscriber can redeem it. A checkout that already applied it keeps its discount. Emits coupon.deactivated.
Stop a code right now. Switching off is the safe way to retire a coupon: no new subscriber can
redeem it, while the redemptions already recorded stay exactly as they are and a buyer who applied
the code before you called keeps the discount they were quoted. Nothing is deleted, so
activate_coupon brings it back.
Why this, and not delete_coupon
delete_coupon refuses while a checkout holding
the code is still in progress, because the discount has already reached a
gateway. Deactivating never refuses for that reason — it is the call to make
when a code has leaked or a campaign has to end this minute. Delete later if
you want the row gone.
Announces coupon.deactivated rather than a generic
coupon.updated, so an automation that reacts to codes being withdrawn does not have to diff
payloads to notice. Re-calling on a code that is already off leaves it unchanged; the event emits on
every successful call, so key a consumer on the coupon's state rather than on counting events.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires one event
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.
Arguments
coupon_id*stringUUID of the coupon to switch off.
What it returns
{ "data": { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "code": "BLACKFRIDAY", "name": "Black Friday 2026", "discount_type": "percentage", "discount_value": "25.0000", "currency": null, "duration": "once", "max_redemptions": 500, "max_redemptions_per_user": 1, "minimum_amount": null, "redemptions": { "count": 137, "remaining": 363, "exhausted": false }, "starts_at": "2026-11-27T00:00:00+00:00", "expires_at": "2026-12-01T00:00:00+00:00", "plan_ids": [], "active": false, "redeemable": false, "created_at": "2026-11-20T09:14:02+00:00" }}The full get_coupon row, with active: false. redeemable is always
false on a deactivated code; redemptions is untouched.
How it fails
VALIDATION_FAILEDthe caller is a team member whose role lacks the team's coupon-update
RESOURCE_NOT_FOUNDunknown coupon_id, a coupon on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:update.
delete_coupon
DESTRUCTIVESoft-delete a coupon code so nobody can redeem it again. Past redemptions are kept. Refused while a checkout holds the code.
Retire a coupon code permanently. The row is soft-deleted: the code disappears from every list and
can never be redeemed again, while the redemptions already recorded and the discounts already
applied stay in the history. Emits coupon.deleted with a
snapshot taken before the row goes, because a consumer reacting to a deletion has no way left to
look the coupon up.
Confirm the target with a human
Deletion cannot be undone from the API. The tool is annotated destructive so a
client can prompt for confirmation; read the code back with
get_coupon and confirm the coupon_id before
calling.
Refused while a checkout is using the code
If a buyer has applied the code and their checkout has not settled, the
discount has already been quoted to a gateway. The service refuses with
VALIDATION_FAILED rather than pull the code out from under them. Call
deactivate_coupon instead: it stops new
redemptions at once and never refuses for a checkout in flight. Delete later,
once the hold has cleared.
Idempotent: a second call on the same id finds nothing and answers RESOURCE_NOT_FOUND, exactly as
an unknown or out-of-scope id does.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires one event
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time.
Arguments
coupon_id*stringUUID of the coupon to delete.
What it returns
{ "data": { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "deleted": true }}id echoes the argument as sent.
How it fails
VALIDATION_FAILEDa checkout using the code is still in progress (error.context.coupon
RESOURCE_NOT_FOUNDunknown coupon_id, a coupon on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:delete.
get_coupon
READFetch one coupon by UUID — discount, limits, redemption tally, plan restriction and whether it is redeemable right now.
Read one coupon code in full. The row is the same one list_coupons
emits, so an agent that spotted a code in the list reads exactly the same fields back here, and the
row a write tool returns after a change is comparable field for field.
Reads go through the coupon service, so the tenant scope and the token's scope:project: allow-list
both apply. An id the token cannot see — unknown, another team's, or on a project outside the
allow-list — answers RESOURCE_NOT_FOUND, indistinguishable from an id that never existed.
Call it before update_coupon or
delete_coupon to read the current state rather than assuming it.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Annotations
It reads and never changes anything.
Arguments
coupon_id*stringUUID of the coupon to fetch.
What it returns
{ "data": { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "code": "BLACKFRIDAY", "name": "Black Friday 2026", "discount_type": "percentage", "discount_value": "25.0000", "currency": null, "duration": "once", "max_redemptions": 500, "max_redemptions_per_user": 1, "minimum_amount": null, "redemptions": { "count": 137, "remaining": 363, "exhausted": false }, "starts_at": "2026-11-27T00:00:00+00:00", "expires_at": "2026-12-01T00:00:00+00:00", "plan_ids": [], "active": true, "redeemable": true, "created_at": "2026-11-20T09:14:02+00:00" }}`plan_ids: []` means every plan, not no plans
An empty array means the code applies to every plan in the project, including plans added later. Do not summarise it as "not applied to any plan".
active is not redeemable. A code can be switched on and still not apply — before
starts_at, after expires_at, or with the cap reached. redeemable is the combined answer for the
coupon as a whole; per-subscriber caps are only resolved at checkout.
Prefer redemptions.remaining over your own subtraction. It is derived from settled redemptions
plus reservations whose hold has not expired, so an in-flight checkout is already counted.
discount_value is a decimal string — parse it as a decimal, not a float. currency is an ISO
code, and null on a percentage coupon.
How it fails
RESOURCE_NOT_FOUNDunknown coupon_id, a coupon on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:view.
list_coupons
READList coupon codes with derived remaining-redemption counts. Read-only. Paginated, optionally narrowed to one project or filtered by on/off state.
Discover existing coupon codes — to report on a campaign, to check whether a code already exists
before calling create_coupon, or to find the id of a code the creator
described by name.
Read-only. Never mutates anything.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Annotations
It reads and never changes anything.
Arguments
project_idstringoptionalOptional project UUID to narrow the list to a single project.
activebooleanoptionalOptional filter: true for codes that are switched on, false for those switched off.
limitintegeroptionalMaximum coupons to return per page (1..100).
1max100pageintegeroptional1-indexed page number.
1What it returns
{ "data": [ { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "code": "BLACKFRIDAY", "name": "Black Friday 2026", "discount_type": "percentage", "discount_value": "25.0000", "currency": null, "duration": "once", "max_redemptions": 500, "max_redemptions_per_user": 1, "minimum_amount": null, "redemptions": { "count": 137, "remaining": 363, "exhausted": false }, "starts_at": "2026-11-27T00:00:00+00:00", "expires_at": "2026-12-01T00:00:00+00:00", "plan_ids": [], "active": true, "redeemable": true, "created_at": "2026-11-20T09:14:02+00:00" } ], "meta": { "page": 1, "limit": 25, "total": 4, "has_more": false }}How it fails
RESOURCE_NOT_FOUNDunknown project_id, or the project is outside the token's scope.
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:view-any.
Three fields worth reading carefully
`plan_ids: []` means every plan, not no plans
An empty array means the code applies to every plan in the project, including plans added later. An agent summarising a coupon must not report it as "not applied to any plan".
Prefer redemptions.remaining over your own subtraction. It is derived, not stored: the quota
counts settled redemptions plus reservations whose hold has not expired, so an in-flight checkout is
already accounted for and an abandoned one returns its slot. max_redemptions - count will
disagree.
active is not redeemable. A code can be switched on and still not apply — outside its
window, fully claimed, or capped for a particular subscriber. redeemable is the combined answer
for the coupon as a whole; per-subscriber caps can only be resolved at checkout.
update_coupon
DESTRUCTIVEChange one or more fields of a coupon code. Partial by design — anything omitted keeps its stored value. Emits coupon.updated.
Change an existing coupon without restating it. Only the arguments present in the call are written,
the way the REST PATCH behaves, so extending an expiry is one field, not a re-creation. Every field
is checked the way create_coupon checks it, and cross-field rules are
checked against the coupon's effective values: a percentage cap is judged against the type the
coupon will have after the call, an end date against the start it will have.
Synchronous — the row after the change comes back in the response, and
coupon.updated emits once with the fields that moved. A call
that carries only coupon_id returns the current row and emits nothing.
Omit to keep, send null to clear
Leaving a field out leaves it alone. To remove a limit or a date, send the key
with null: max_redemptions (unlimited), minimum_amount (no minimum),
starts_at (usable now) and expires_at (no end date) all accept it.
max_redemptions_per_user does not — it is always at least 1.
`plan_ids: []` widens the code to every plan
Omitting plan_ids leaves the restriction as it is. Sending an empty list
deliberately covers every plan in the project, including ones added later. To
narrow a code to "the Premium plan", pass that plan's UUID; resolve it with
list_plans first.
Requires the Coupons Addon or a Growth plan on the project owner. When the
tier has lapsed the call fails with TEAM_TIER_REQUIRED and nothing is
written. The discount still applies to the first payment only; nothing
here changes a subscriber's renewal price.
Switching discount_type has a side effect worth knowing: moving to fixed needs a currency_id
unless the coupon already stores one, and moving to percentage clears the stored currency.
Requires ability
The token behind the MCP session must hold it, or the call is refused with TOKEN_MISSING_ABILITY.
Runs the same action as
The REST endpoint and this tool share one action, so validation, permissions and events are identical.
Fires one event
Delivered to every endpoint subscribed to it once the change is made.
Annotations
A client that honours annotations asks a person before running it. Sending the same arguments twice changes nothing the second time. It reaches beyond Subscriby: a connector, a provider or a member.
Arguments
coupon_id*stringUUID of the coupon to change.
codestringoptionalNew code subscribers type at checkout. Letters, numbers and dashes only, 3 to 64 characters. Stored uppercase; must not already exist in the project.
namestringoptionalInternal label, 3 to 255 characters.
discount_typestringoptionalEither "percentage" or "fixed". Switching to fixed needs a currency_id unless the code already has one.
discount_valuenumberoptionalFor percentage, 1..100. For fixed, an amount greater than zero in the coupon's currency.
currency_idstringoptionalCurrency UUID for a fixed discount. Ignored and cleared for a percentage.
max_redemptionsintegeroptionalTotal uses allowed across everyone. Pass null for unlimited.
1max_redemptions_per_userintegeroptionalHow many times one subscriber may use the code.
1minimum_amountnumberoptionalOnly apply the code when the plan costs at least this much. Pass null for no minimum.
starts_atstringoptionalISO 8601 instant the code becomes usable. Pass null to start immediately.
expires_atstringoptionalISO 8601 instant the code stops working. Pass null for no end date. Must fall after the start.
activebooleanoptionalWhether the code is usable.
plan_idsstring[]optionalPlan UUIDs to restrict the code to. An empty list covers every plan in the project, including ones added later.
What it returns
{ "data": { "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594", "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13", "code": "BLACKFRIDAY", "name": "Black Friday 2026 (extended)", "discount_type": "percentage", "discount_value": "25.0000", "currency": null, "duration": "once", "max_redemptions": 750, "max_redemptions_per_user": 1, "minimum_amount": null, "redemptions": { "count": 137, "remaining": 613, "exhausted": false }, "starts_at": "2026-11-27T00:00:00+00:00", "expires_at": "2026-12-08T00:00:00+00:00", "plan_ids": [], "active": true, "redeemable": true, "created_at": "2026-11-20T09:14:02+00:00" }}The whole row comes back, not only the fields sent, so there is no need for a follow-up
get_coupon. code is uppercase whatever was sent; discount_value is a
decimal string. redemptions.remaining is recomputed against the new cap.
How it fails
TEAM_TIER_REQUIREDthe project owner's tier no longer includes coupons. error.context.reason
VALIDATION_FAILEDone entry per offending field in error.context: code outside 3–64
RESOURCE_NOT_FOUNDunknown coupon_id, a coupon on another team's project, one outside the
AUTHENTICATION_REQUIREDno authenticated user on the request.
TOKEN_MISSING_ABILITYtoken lacks project-coupon:update.
How is this guide?