Create a coupon
The coupons.create operation: POST /api/v1/coupons in the REST API and the coupons_create tool in the MCP server.
/api/v1/couponsCreates a coupon in the brand's catalog, in the request's mode: a percent off (percentOffPpm, parts per
million: 500000 = 50%) or an amount off (amountOff, whole minor units, only for invoices in its currency),
for once (one billing period), repeating (durationInPeriods periods) or forever.
Optionally limit it to products or prices (appliesTo) and cap redemptions with maxRedemptions and
redeemBy. Customers redeem coupons through promotion codes (promotion_codes.create); operators apply them
to subscriptions directly. Once a coupon is used, only its name, status and metadata can change.
At a glance
- REST:
POST /api/v1/coupons - MCP tool:
coupons_create - Operation ID:
coupons.create - Scope: Brand
- Permission:
catalog:write - Risk tier:
write - Dry run: Supported (send
Ownrecord-Dry-Run: true) - Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: API key with the
catalog:writescope - Since: M5
Request
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Shown to customers at checkout and on invoices, e.g. Launch offer. |
percentOffPpm | string | No | Percent off in parts per million as a string: 500000 = 50%, 1000000 = 100%. Exactly one of this and amountOff. |
amountOff | object | No | An amount off per invoice, in whole minor units of its currency; applies only to invoices in that currency. Exactly one of this and percentOffPpm. |
amountOff.amount | string | Yes | Integer micros as a string (12500000 = 12.50). |
amountOff.currency | string | Yes | ISO 4217 currency code: the price's currency. |
duration | one of once, repeating, forever | Yes | once (one billing period), repeating (durationInPeriods periods) or forever. Periods count from the first period boundary at or after the discount starts. |
durationInPeriods | integer | No | Repeating coupons only: how many billing periods the discount lasts (1–120). |
appliesTo | object | No | Limit the coupon to lines of these products or prices (a line matches either). Absent: every line. |
appliesTo.productIds | array of prod_… ID | No | Only lines of these products. |
appliesTo.priceIds | array of price_… ID | No | Only lines of these prices, e.g. a monthly price but not the yearly one. |
maxRedemptions | integer | No | How many times it can be redeemed in total (counted on successful payment). |
redeemBy | date-time | No | No new redemptions from this instant on (must lie ahead). |
metadata | object | No | Your own key-value pairs (at most 50 keys). On updates the whole set is replaced. |
Response
201 Created with a JSON object:
| Field | Type | Description |
|---|---|---|
id | cpn_… ID | |
object | "coupon" | |
brandId | brd_… ID | |
livemode | boolean | |
name | string | |
percentOffPpm | string or null | Percent off in parts per million (500000 = 50%), or null for an amount-off coupon. |
amountOff | money or null | Amount off per invoice, or null for a percent-off coupon. |
duration | one of once, repeating, forever | |
durationInPeriods | integer or null | |
appliesTo | object or null | The products and prices the coupon is limited to, or null for every line. |
maxRedemptions | integer or null | |
redeemBy | date-time or null | |
timesRedeemed | integer | |
valid | boolean | Whether it can be redeemed now: active, before redeemBy, below its limit. |
used | boolean | A discount references it or a redemption was counted: only name, status and metadata can change. |
active | boolean | |
metadata | object | |
createdAt | date-time | |
updatedAt | date-time |
Examples
50% off the monthly price for three months
POST /api/v1/coupons
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"name": "Launch offer",
"percentOffPpm": "500000",
"duration": "repeating",
"durationInPeriods": 3,
"appliesTo": {
"priceIds": [
"price_01m4tx5nj8emwbyf7y9vcf47y2"
]
},
"maxRedemptions": 500,
"redeemBy": "2027-01-01T00:00:00.000Z"
}
Response (201):
{
"id": "cpn_01m4tx5nj8emwbyf7y9vcf47z0",
"object": "coupon",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"livemode": false,
"name": "Launch offer",
"percentOffPpm": "500000",
"amountOff": null,
"duration": "repeating",
"durationInPeriods": 3,
"appliesTo": {
"productIds": [],
"priceIds": ["price_01m4tx5nj8emwbyf7y9vcf47y2"]
},
"maxRedemptions": 500,
"redeemBy": "2027-01-01T00:00:00.000Z",
"timesRedeemed": 0,
"valid": true,
"used": false,
"active": true,
"metadata": {},
"createdAt": "2026-10-13T12:00:00.000Z",
"updatedAt": "2026-10-13T12:00:00.000Z"
}
€10 off one invoice
POST /api/v1/coupons
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"name": "€10 welcome credit",
"amountOff": {
"amount": "10000000",
"currency": "EUR"
},
"duration": "once"
}
Response (201):
{
"id": "cpn_01m4tx5nj8emwbyf7y9vcf47z1",
"object": "coupon",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"livemode": false,
"name": "€10 welcome credit",
"percentOffPpm": null,
"amountOff": {
"amount": "10000000",
"currency": "EUR"
},
"duration": "once",
"durationInPeriods": null,
"appliesTo": null,
"maxRedemptions": null,
"redeemBy": null,
"timesRedeemed": 0,
"valid": true,
"used": false,
"active": true,
"metadata": {},
"createdAt": "2026-10-13T12:00:00.000Z",
"updatedAt": "2026-10-13T12:00:00.000Z"
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | Not exactly one of percentOffPpm and amountOff, a value out of bounds, a duration without (or with an unexpected) number of periods, or redeemBy in the past. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | A product or price in appliesTo isn't in this brand and mode. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
permission_denied | 403 | You can't change this brand's catalog. | Ask an organization owner or admin for a role or scope that allows this action. |
It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.