Promotions allow you to offer discounts, credits, and special pricing to customers through coupon codes, automatic rules, or manual application.
Key concepts:
- Code - the coupon code a customer enters (e.g.,
SAVE20) - Effects - what the promotion does: percentage discount, fixed discount, free trial extension, or credit grant
- Conditions - eligibility rules: minimum spend, specific plans, customer segments, date ranges
- Budget - optional spending cap; the promotion deactivates when the budget is exhausted
- Stackable - whether this promotion can combine with other active promotions
- Simulation - preview the financial impact of a promotion before activating it
List a customer's promotion redemptions
Returns a customer's promotion redemptions, newest first. Cursor-paginated (default 50, max 200 per page).
path Parameters
idCustomer UUID
query Parameters
statusFilter by redemption status (active|dormant|pending|expired|revoked)
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List a customer's promotion redemptions › Responses
OK
List promotions
Returns a paginated list of promotions. Supports the unified field.op=value filter grammar (see /docs/pagination).
query Parameters
archetypeFilter on archetype (enum). Operators: eq, in — dot grammar, e.g. archetype.in=value; a bare archetype=value means eq. A bare comma-separated value is in-sugar: archetype=a,b means archetype.in=a,b. Legal values: generic, coupon_discount, bundle_discount, volume_rebate, first_purchase_credit, free_trial, cross_sell_free, loyalty_ladder, growth_ladder.
codeFilter on code (string). Operators: eq, contains, in — dot grammar, e.g. code.contains=value; a bare code=value means eq. A bare comma-separated value is in-sugar: code=a,b means code.in=a,b.
created_atFilter on created_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. created_at.gt=value; a bare created_at=value means eq.
distributionFilter on distribution (enum). Operators: eq, in — dot grammar, e.g. distribution.in=value; a bare distribution=value means eq. A bare comma-separated value is in-sugar: distribution=a,b means distribution.in=a,b. Legal values: AUTO_APPLY, AUTO_ENROLL, COUPON, DIRECT.
nameFilter on name (string). Operators: eq, contains — dot grammar, e.g. name.contains=value; a bare name=value means eq.
stacking_groupFilter on stacking_group (string). Operators: eq, in — dot grammar, e.g. stacking_group.in=value; a bare stacking_group=value means eq. A bare comma-separated value is in-sugar: stacking_group=a,b means stacking_group.in=a,b.
stacking_modeFilter on stacking_mode (enum). Operators: eq, in — dot grammar, e.g. stacking_mode.in=value; a bare stacking_mode=value means eq. A bare comma-separated value is in-sugar: stacking_mode=a,b means stacking_mode.in=a,b. Legal values: stack, exclusive.
updated_atFilter on updated_at (date-time). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. updated_at.gt=value; a bare updated_at=value means eq.
valid_fromFilter on valid_from (date). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. valid_from.gt=value; a bare valid_from=value means eq.
valid_toFilter on valid_to (date). Operators: eq, gt, gte, lt, lte — dot grammar, e.g. valid_to.gt=value; a bare valid_to=value means eq.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
sortOrdering: created_at, with an optional :asc/:desc suffix (default created_at:desc). An unknown value is rejected. Cursors are bound to the ordering that issued them, so changing sort mid-walk needs a fresh first page. Supplying a sort turns OFF relevance ranking when ?search= is present, and a cursor cannot be carried across that switch: relevance is a different total order.
searchCase-insensitive substring match over name and code; relevance-ranked (exact > prefix > substring)
statusFilter by status (sugar for status.eq). Filterable fields (status, distribution, name, code, archetype, stacking_mode, stacking_group, valid_from, valid_to, created_at, updated_at) accept apifilter operator suffixes in the dot grammar, e.g. status.in=a,b
derived_statusPost-filter on the computed derived_status DTO field
countsComma-separated countable fields (status, archetype) to include per-value counts for
has_scheduled_versionFilter by whether the entity has a version scheduled to publish (true or false). A scheduled version is a FROZEN version row a pending catalog.publish_version Change is bound to; the entity itself stays draft, published or archived and its working draft stays free for immediate changes.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List promotions › Responses
OK
Create a promotion
Creates a new promotion in draft status.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Create a promotion › Request Body
distributionHow a customer comes to hold the promotion: AUTO_APPLY, AUTO_ENROLL, COUPON, or DIRECT. Required.
nameOperator-facing promotion name.
valid_fromFirst calendar day (UTC) the promotion is valid; required.
announcement_audienceWho this promotion's activation announcement is emailed to: promotion_audience (the promotion's own audience decides, and the per-email audience set for this kind is waived) or email_audience (that per-email audience applies too, so the announcement reaches the intersection of the two). The WORKSPACE-wide email audience on mail_defaults narrows both — it is a standing statement about who receives customer email at all and a promotion cannot reach past it. Omit to take the workspace creation preset. Decides who is emailed only — it never changes who may enrol in or redeem the promotion.
application_timingWhen a redeemed promotion first takes effect: NEXT_CYCLE, RETROACTIVE, or IMMEDIATE.
archetypeFrontend-only preset hint; not persisted as rule logic.
Live ENROLMENT audience: who may hold this promotion. Send audience.definition to set the rule tree (applying a saved audience copies its rules in); the four flat fields are the legacy shape and are ignored when a definition is present. Omit for all customers.
benefit_on_window_closeWhat valid_to means for customers who already redeemed: end (default) expires their benefit with the promotion; keep leaves each redemption running to its own benefit duration. Either way, past valid_to nobody new can redeem.
budget_behaviorWhat happens when a redemption would exceed max_budget: SKIP or PARTIAL.
budget_currencyISO 4217 currency the budget is denominated in; required when max_budget is set.
codeCoupon code; required when distribution is COUPON and must be omitted otherwise.
descriptionInternal description of the promotion (not shown to customers).
effectsSingle-phase shorthand: the effects to apply. Mutually exclusive with phases.
Single-phase shorthand: the eligibility condition set. Mutually exclusive with phases.
evaluation_scopeWhere the engine evaluates the promotion: invoice (default), subscription, usage_event, or reserve.
max_budget^-?\d+(\.\d+)?$MAJOR units, denominated in budget_currency (required when max_budget is set).
max_redemptionsCap on total redemptions across all customers; omit for unlimited.
max_redemptions_per_customerCap on redemptions by any single customer; omit for unlimited.
Phased-ladder rule shape: ordered phases with per-phase conditions and effects. Mutually exclusive with eligibility/effects.
priorityOrdering priority on the invoice pass; higher applies first and wins within a stacking group.
public_descriptionCustomer-facing description of the promotion.
stacking_groupNamed mutual-exclusion group for stacking_mode=stack; promotions sharing a group never combine with each other.
stacking_modeHow the promotion combines with others on the invoice pass: stack (default) or exclusive (applies alone).
Localized overrides for customer-facing text, keyed by locale.
valid_toLast calendar day (UTC) the promotion is valid, inclusive; null = open-ended.
Create a promotion › Responses
Created
Get where new promotions start
The values a NEW promotion is created with when its author leaves those fields blank: how it stacks, what it does when its budget runs out, when it takes effect, its ordering priority, the preset it is filed under, whether its conditions are ALL or ANY, and who its activation announcement is emailed to. That last one decides who is EMAILED and never who may enrol in or redeem a promotion. ONLY EVER APPLIED AT CREATION — changing these does not alter a single promotion that already exists, because every promotion carries the values it was created with. ALWAYS ANSWERS: a workspace that has never set any of this gets the empty preset with source="default" rather than a 404, and "built_in" always reports the values the system falls back to, so a create form can show an author what a blank field will inherit. Stored per WORKSPACE — your other workspaces are untouched by it. Requires promotion.read.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get where new promotions start › Responses
OK
Set where new promotions start
Replaces this workspace's promotion preset. Every field is optional and LEAVING ONE OUT CLEARS IT — an absent field means there is no preset for it and new promotions fall back to the built-in value, which is how a preset gets un-set. THIS CHANGES NO EXISTING PROMOTION: the preset is read once, when a promotion is created, and its values are written onto that promotion for good. Fixing a promotion that is already running is an edit to that promotion, not to this. An author can still override any of these on the create form itself. Requires promotion.write.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Set where new promotions start › Request Body
announcement_audienceWho a NEW promotion's activation announcement is emailed to: promotion_audience (the promotion's own audience decides, and the audience set for that one email is waived) or email_audience (that per-email audience applies too, so the announcement reaches the intersection of the two). The WORKSPACE-wide email audience on mail_defaults narrows both — it is a standing statement about who receives customer email at all and a promotion cannot reach past it. Omit for no preset. Decides who is EMAILED only; it never changes who may enrol in or redeem a promotion.
application_timingWhen a NEW promotion first takes effect once redeemed. Omit for no preset, which means the next billing cycle.
archetypeThe authoring preset a NEW promotion is filed under. A display and filter hint, not rule logic. Omit for no preset, which means generic.
budget_behaviorWhat a NEW promotion does when a redemption would exceed its budget cap: SKIP it entirely, or apply PARTIAL up to what is left. Omit for no preset, which means SKIP.
condition_modeWhether a NEW promotion requires ALL of its eligibility conditions to pass or ANY one of them. Omit for no preset, which means ALL.
priorityOrdering priority a NEW promotion starts with; higher applies first and wins inside a stacking group. Omit for no preset, which means 0 — and note that presetting 0 deliberately is a real choice this field can express, distinct from omitting it.
stacking_modeHow a NEW promotion combines with others on the invoice pass. Omit for no preset, which means it stacks. Only promotions created after this is set are affected — existing promotions keep whatever they were created with.
Set where new promotions start › Responses
OK
Clear where new promotions start
Forgets the preset entirely; new promotions go back to the built-in values, and the GET keeps answering with the empty preset. Idempotent, and never refused — promotions created while the preset existed keep the values they were created with, so there is nothing this could break. Requires promotion.write.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Clear where new promotions start › Responses
No Content
Redeem a promotion using a coupon token
Redeems a promotion for a customer using a customer-locked coupon token.
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Redeem a promotion using a coupon token › Request Body
customer_idCustomer redeeming the token; must match the customer the token is locked to. Required.
tokenThe customer-locked coupon token to redeem; required.
subscription_idSubscription to scope the redemption to, when applicable.
Redeem a promotion using a coupon token › Responses
Created
Simulate promotion impact
Runs the invoice pipeline without persisting invoices or incrementing promotion budgets.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Simulate promotion impact › Request Body
billing_period_endLast day (UTC) of the billing period to simulate; must be after billing_period_start.
billing_period_startFirst day (UTC) of the billing period to simulate; required.
customer_idCustomer to simulate the invoice for; required.
promotion_idsPromotions to apply in the simulation; at least one is required.
subscription_idSubscription whose charges are simulated; required.
Simulate promotion impact › Responses
OK
Validate promotion eligibility
Checks which promotions a customer is eligible for.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Validate promotion eligibility › Request Body
customer_idCustomer to check promotion eligibility for; required.
coupon_codeCoupon code to include coupon-gated promotions in the eligibility check.
invoice_subtotalDecimal string, MAJOR units. Currency is the evaluated customer/subscription context's currency.
subscription_idSubscription context to evaluate against, when applicable.
Validate promotion eligibility › Responses
OK
Get a promotion
Returns a single promotion with derived read-time fields: a derived_status evaluated against the current instant plus remaining budget and remaining redemptions where caps are set.
path Parameters
idPromotion UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get a promotion › Responses
OK
Delete a promotion
Soft-deletes a draft promotion.
path Parameters
idPromotion UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Delete a promotion › Responses
No Content
Update a promotion
Merge-patch update: only supplied fields change, and none of them publish. An edit to a DRAFT promotion lands in place — a draft promotion is its own working copy. An edit to a LIVE promotion STAGES on its working draft version, so a promotion that is matching and redeeming does not change under its own redeemers; read that draft with GET /v1/promotions/{id}/draft and discard it with DELETE. An ARCHIVED promotion is terminal and returns 409. Taking a promotion live — the first publish and every staged edit after it — is POST /v1/changes with operation catalog.publish_version and subject_kind=promotion; there is no effective_at and no save_as_draft. valid_from/valid_to are CONTENT: they are the promotion's own redeemable window and self-execute, independently of when it was published. expected_version is the optimistic-concurrency token: when supplied and stale, the update returns 409.
path Parameters
idPromotion UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Update a promotion › Request Body
announcement_audienceWho this promotion's activation announcement is emailed to: promotion_audience (the promotion's own audience decides, and the per-email audience set for this kind is waived) or email_audience (that per-email audience applies too). The WORKSPACE-wide email audience narrows both. Omit to leave unchanged. Decides who is emailed only — it never changes who may enrol in or redeem the promotion.
application_timingNew application timing (NEXT_CYCLE, RETROACTIVE, IMMEDIATE); omit to leave unchanged.
archetypeFrontend-only preset hint; not persisted as rule logic.
New ENROLMENT audience; a full replacement of the live audience when set (no version bump). Send audience.definition to replace the rule tree; a request carrying only the legacy flat fields is refused when the stored audience says more than those fields can hold.
benefit_on_window_closeWhat valid_to means for customers who already redeemed: end expires their benefit with the promotion; keep leaves each redemption running to its own benefit duration. Omit to leave unchanged. Either way, past valid_to nobody new can redeem.
budget_behaviorNew over-budget behavior (SKIP or PARTIAL); omit to leave unchanged.
budget_currencyISO 4217 currency the budget is denominated in; required (on the merged state) when max_budget is set.
codeNew coupon code; only valid when distribution is COUPON (biconditional enforced on the merged state).
descriptionNew internal description; omit to leave unchanged.
distributionNew distribution axis (AUTO_APPLY, AUTO_ENROLL, COUPON, DIRECT); omit to leave unchanged.
effectsSingle-phase shorthand effects; full replacement of the rule when set. Mutually exclusive with phases.
Single-phase shorthand eligibility set; full replacement of the rule when set. Mutually exclusive with phases.
evaluation_scopeNew evaluation scope (invoice, subscription, usage_event, reserve); omit to leave unchanged.
expected_versionOptimistic-concurrency token: send the version you last read and a stale value answers 409 CONFLICT. Omitted: the update applies unconditionally (last write wins).
max_budget^-?\d+(\.\d+)?$MAJOR units, denominated in budget_currency.
max_redemptionsNew total-redemptions cap; omit to leave unchanged.
max_redemptions_per_customerNew per-customer redemptions cap; omit to leave unchanged.
nameNew operator-facing name; omit to leave unchanged.
Phased-ladder rule; full replacement when set. Mutually exclusive with eligibility/effects.
priorityNew invoice-pass ordering priority; omit to leave unchanged.
public_descriptionNew customer-facing description; omit to leave unchanged.
stacking_groupNew mutual-exclusion group; an empty string clears the group, omission leaves it unchanged.
stacking_modeNew stacking mode (stack or exclusive); omit to leave unchanged.
Localized overrides for customer-facing text, keyed by locale.
valid_fromNew first valid calendar day (UTC); omit to leave unchanged.
valid_toNew last valid calendar day (UTC), inclusive; omit to leave unchanged.
Update a promotion › Responses
OK
Archive a promotion (terminal)
Terminally retires an active promotion: no new redemptions, and there is no way back to active - create or copy a new promotion instead. Already-applied benefits on issued invoices are untouched; only future invoices stop receiving them.
path Parameters
idPromotion UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Archive a promotion (terminal) › Responses
OK
Preview a promotion's audience
Returns the customers matching an audience definition plus the total match count and each customer's enrollment status. Omit the audience body to preview the stored audience; pass one to preview unsaved editor state.
path Parameters
idPromotion UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview a promotion's audience › Responses
OK
Re-fire a promotion's marketing broadcast
Re-runs the promotion's marketing fan-out so each recipient passes the per-recipient consent check and the one-click unsubscribe header is minted. This is the remedy for mail blocked as no_unsubscribe_configured, which can NEVER be retried from the mail history: that block happened in the producer, and re-sending a stored message cannot mint the header it was blocked for. Requires expected_recipients from the dry run; the audience is re-counted and ANY difference refuses the request, sending nothing. Refused with 409 when nothing would send, naming which gate is closed. Asynchronous: the fan-out is enqueued and paces itself, honouring a promotional pause mid-flight.
path Parameters
idPromotion UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Re-fire a promotion's marketing broadcast › Request Body
expected_audience_revisionThe audience.revision returned by the dry run. Echo it back exactly: any edit to the audience rules bumps it, so a re-fire confirmed against the old revision is refused even when the count is unchanged. Send 0 when targeting is "everyone" — a match-all promotion has no audience row and the dry run reports no revision.
expected_recipientsThe recipients count returned by the dry run. Required. Re-counted server-side; any difference refuses the request rather than sending to a population nobody reviewed.
reasonWhy the broadcast is being re-fired. Required, and recorded on the durable event.
expected_audience_idThe audience.id returned by the dry run. Echo it back when targeting is "audience": a re-fire is refused if the promotion has been re-pointed at a different audience since, which the revision alone cannot detect. Omit when targeting is "everyone" — a match-all promotion has no audience row.
Re-fire a promotion's marketing broadcast › Responses
OK
Dry-run a marketing broadcast re-fire
MANDATORY DRY RUN for re-firing a promotion's marketing email. Returns the number of mailable customers the audience matches right now, plus the gates between that audience and a delivered message that are decided ONCE for the whole broadcast (email switched off, promotional email paused, no enabled promotion.activated template bound). IT IS AN UPPER BOUND, not an exact figure: the email audience is applied PER RECIPIENT at send time and this count does not run it, so a workspace that has set one will see the send land short of this number, with the difference recorded as visible outside_audience messages. The error is always in that direction — you are never told fewer people will be mailed than are. The recipients count must be sent back as expected_recipients to confirm; a count this endpoint could not complete (truncated=true) cannot be confirmed at all. A re-fire never delivers a second copy to a customer already sent this broadcast - only recipients who were blocked or never sent can receive one.
path Parameters
idPromotion UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Dry-run a marketing broadcast re-fire › Responses
OK
Copy a promotion as a new draft
Clones an existing promotion (any status) into a new draft promotion. Conditions, effects, and phases are duplicated.
path Parameters
idSource promotion UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Copy a promotion as a new draft › Request Body
valid_fromFirst calendar day (UTC) the new draft is valid; required.
codeCoupon code for the new draft; only valid when the promotion's distribution is COUPON.
nameName for the new draft; defaults to the source promotion's name suffixed as a copy when omitted.
valid_toLast calendar day (UTC) the new draft is valid, inclusive; null = open-ended.
Copy a promotion as a new draft › Responses
Created
Get the staged draft
The entity's open draft: its row id (the same id catalog.publish_version binds as staged_version_id), the version number it will publish as, when it was opened and last touched, the head version it forks from, and a counted summary of what is staged. 404 when nothing is staged — which is a state, not an error.
path Parameters
idThe entity's UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get the staged draft › Responses
OK
Discard the staged draft
Throws the staged draft away. The live entity is untouched: nothing pointed at the draft and it was never on the billing timeline. 404 when nothing is staged, which is a different answer from a successful discard and is reported as one.
path Parameters
idThe entity's UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Discard the staged draft › Responses
No Content
Bulk-enroll customers into a promotion
Synchronously enrolls explicit customers and/or every current audience match (sweep semantics). Each enrollment goes through the regular redeem path, so caps, conditions, and budget all apply. Returns a per-customer outcome list.
path Parameters
idPromotion UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Bulk-enroll customers into a promotion › Request Body
customer_idsfrom_audienceBulk-enroll customers into a promotion › Responses
OK
Preview promotion eligibility
Evaluates a single promotion's conditions for a customer and returns per-condition pass/fail details.
path Parameters
idPromotion UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview promotion eligibility › Request Body
customer_idCustomer to evaluate the promotion's conditions for; required.
subscription_idSubscription context to evaluate against, when applicable.
Preview promotion eligibility › Responses
OK
Redeem a promotion
Redeems a promotion for a customer, creating a redemption record.
path Parameters
idPromotion UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Redeem a promotion › Request Body
customer_idCustomer to redeem the promotion for; required.
coupon_codeCoupon code, required when the promotion's distribution is COUPON.
subscription_idSubscription to scope the redemption to, when applicable.
Redeem a promotion › Responses
Created
List a promotion's redemptions
Returns the customers who have redeemed (or currently hold) the promotion, newest first. Cursor-paginated (default 50, max 200 per page).
path Parameters
idPromotion UUID
query Parameters
statusFilter by redemption status (active|dormant|pending|expired|revoked)
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List a promotion's redemptions › Responses
OK
Preview a benefit clawback
Prices what revoking with revert_benefits would reverse — per-currency totals from the applied-promotions invoice ledger plus wallet credits granted by this redemption. Moves no money.
path Parameters
idPromotion UUID
redemptionIdRedemption UUID
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Preview a benefit clawback › Responses
OK
Opt a customer out of a promotion
Neutral departure: benefits stop forward, past benefits keep, auto-enrollment never re-adds, explicit re-join stays possible.
path Parameters
idPromotion UUID
redemptionIdRedemption UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Opt a customer out of a promotion › Responses
OK
Move a redemption's phase by hand (repair)
REPAIR TOOL, not part of the normal lifecycle - phases advance and regress on their own as conditions are met. Use it when a redemption's phase pointer is unusable (unset, or naming a phase that is not one of this promotion's), which BLOCKS that customer's invoicing indefinitely: the engine refuses to finalize rather than guess which effects apply, and the fault is stored state every retry re-reads. Moving the phase REPRICES future invoices - the target phase's effects are snapshotted onto the redemption and take precedence from the next invoice on - so reason is required and lands on the recorded transition (direction=manual, trigger_type=admin). Omit to_phase_id for the entry phase. Only active and dormant redemptions can be repaired; anything else no longer prices an invoice. Returns recorded=false, having written nothing, when an automatic transition won the race.
path Parameters
idPromotion UUID
redemptionIdRedemption UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Move a redemption's phase by hand (repair) › Request Body
reasonWhy the phase is being moved by hand. Required, and recorded on the transition.
to_phase_idThe phase of THIS promotion to point the redemption at. Omit for the promotion's entry phase.
Move a redemption's phase by hand (repair) › Responses
OK
Revoke a redemption (blacklist, optionally clawing back benefits)
Operator action: terminally revokes a redemption and BLACKLISTS the customer (redeem is rejected until unblock). Optionally claws back granted benefits with revert_benefits. This is distinct from opt-out, which is a neutral, rejoinable stop that never claws back - use opt-out for a customer-initiated departure, revoke to remove and bar.
path Parameters
idPromotion UUID
redemptionIdRedemption UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Revoke a redemption (blacklist, optionally clawing back benefits) › Request Body
revert_benefitsWhen true, additionally claws back everything the redemption delivered (invoice-ledger benefits and wallet credit grants) via a signed wallet adjustment. Destructive and not restored by a later re-join.
Revoke a redemption (blacklist, optionally clawing back benefits) › Responses
OK
Unblock a revoked (blacklisted) redemption
Lifts the blacklist: revoked → opted_out. The customer may be enrolled again; nothing previously consumed or clawed back is restored.
path Parameters
idPromotion UUID
redemptionIdRedemption UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Unblock a revoked (blacklisted) redemption › Responses
OK
List coupon tokens
Returns paginated coupon tokens for a promotion.
path Parameters
idPromotion UUID
query Parameters
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List coupon tokens › Responses
OK
Generate coupon tokens
Generates customer-locked single-use coupon tokens for a promotion.
path Parameters
idPromotion UUID
Headers
Idempotency-KeyUnique key that makes this POST safe to retry: repeats with the same key replay the first response instead of re-executing. Replays are scoped to the retrying principal (same API key / user) and kept for 24 hours. Required on every POST.
Client-generated idempotency key (e.g. a UUID).
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Generate coupon tokens › Request Body
countNumber of coupon tokens to generate (1-10000); larger campaigns repeat the call.
emailsOptional customer emails to lock the tokens to, one per token; when provided its length must equal count.
expires_atOptional expiry stamped on every token generated by this call; after it a token can no longer be redeemed. Does NOT affect anyone who already redeemed — for that, see the promotion's own valid_to.
Generate coupon tokens › Responses
Created
List promotion versions (with lifecycle filter)
Returns version rows for the entity, keyset-paginated (default 100, max 200 per page). The status filter accepts draft, scheduled, published, archived; the default excludes draft (ADR-0007). Supports ?include=usage_count and ?archived_reason=.
path Parameters
idEntity UUID
query Parameters
statusLifecycle status filter (repeatable / comma-separated): draft, scheduled, published, archived. Default excludes draft.
archived_reasonNarrow archived rows by reason (repeatable / comma-separated): canceled, discarded, errored, superseded
includeComma-separated includes. Currently supports usage_count.
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List promotion versions (with lifecycle filter) › Responses
OK
Get a promotion version snapshot
Returns the static snapshot row for one promotion version (naming, code, distribution, stacking, budget, validity, and release fields). The version's linked phases, conditions, and effects are fetched separately when drilling in.
path Parameters
idEntity UUID
versionVersion number
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get a promotion version snapshot › Responses
OK
Name or annotate a promotion version
Sets the version's release_name and/or release_note. These are display metadata, not content: editing them never creates a new version and is allowed on published versions. An absent field is left unchanged; an empty string clears it.
path Parameters
idEntity UUID
versionThe version number
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Name or annotate a promotion version › Request Body
release_nameNew name; empty string clears it. Absent leaves it unchanged.
release_noteNew note; empty string clears it. Absent leaves it unchanged.
Name or annotate a promotion version › Responses
OK
created_ateffective_fromeffective_toidoriginWho made this version: manual (an operator published it), scheduled_release (a scheduled publish fired), cascade (republished automatically because something it depends on published).
published_atstatusversionarchived_reasonFor origin=cascade: the version whose publish caused this one, resolved to its entity and number.
created_byrank1-based position in the entity's queue of scheduled versions — the order they will fire. Present only on scheduled rows.
release_nameOperator-given name of this version, shown beside its number.
release_noteFree-text note about this version.
scheduled_change_idThe pending catalog.publish_version Change that will publish this version — reschedule or cancel it there.
scheduled_forWhen this scheduled version is due to publish. Present only on the row a pending catalog.publish_version Change is bound to.
usage_countForecast the impact of superseding this promotion version
Who is using the version in force today, and what a publish over it would move. pinnable reports whether holding a version is possible for this entity at all — it is false for tax rules, which are dated rather than pinned, so a caller must not render a pinned count for them. republishes_consumers is the cross-entity fan-out (publishing a price formula republishes every ACTIVE product whose head tiers reference it), and live_readers counts consumers billing off the live head, who move as soon as the publish lands. Read-only, and uncached: this is read at the moment money changes.
path Parameters
idEntity UUID
versionVersion number (positive integer)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Forecast the impact of superseding this promotion version › Responses
OK
List promotion version referrers
Offset-paginated drill-down of the referrers of one type for one version of the entity.
path Parameters
idEntity UUID
versionVersion number (positive integer)
query Parameters
typeReferrer type to list (required)
limitPage size. Values above the server-side cap are clamped (standard default 50, cap 200; a few document-heavy lists use larger windows). Invalid values fall back to the default.
cursorOpaque continuation token from the previous response's pagination.cursor. Omit for the first page. Cursors are stateless and do not expire, but are only valid for the list and filters that produced them.
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
List promotion version referrers › Responses
OK
Get promotion version usage summary
Referrer counts for one version of the entity, grouped by referrer type. Powers the version-switcher hover card.
path Parameters
idEntity UUID
versionVersion number (positive integer)
Headers
X-Workspace-IdSelects the workspace this request operates in, by workspace UUID or slug (e.g. a sandbox workspace for test integrations). Omitted: the organization's default live workspace. Unknown workspace: 404; workspace outside your organization: 403. Discover workspaces via GET /workspaces.
Workspace UUID or slug.
Get promotion version usage summary › Responses
OK