> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Coupons & Promotion Codes

> Waffo's discount engine end to end: percent-off vs amount-off coupons, Once / Repeating / Forever duration, single and batch promotion codes, redemption records, discount analytics, account-level caps, subscription trials, refund reversal, and the two API entry points.

Waffo has a first-party discount engine. You define **coupons** (what the discount is) and issue **promotion
codes** (the strings customers type, or that you distribute), then read redemptions and discount spend back out
of the Portal. It all lives under the **Marketing** menu: Coupons, Promotion Codes, Discount Analytics and
Coupon Settings. The feature is permission-gated — if the Marketing menu is visible in your Portal, it is
available on your account.

## How do I define the discount itself?

| Discount type | What you configure | How it applies |
| - | - | - |
| `PERCENT_OFF` | One integer percentage, 1–100 | Applies in any currency. 100 makes the order free |
| `AMOUNT_OFF` | A fixed amount **per currency** | Applies only in the currencies you configured |

A currency left unconfigured on an `AMOUNT_OFF` coupon is **not** eligible — an order in that currency is
refused rather than silently charged full price. Configure every currency you sell in.

Duration counts **billing cycles**, so it only bites on subscriptions; a one-time payment consumes the discount
once either way. `ONCE` — a single payment only, and a second attempt on the same discount is refused.
`REPEATING` — N cycles, where you also set **Duration in Cycles**. `FOREVER` — every cycle.

## What else do I fill in when creating a coupon?

| Field | Rules worth knowing |
| - | - |
| **Coupon Name** | Customer-facing, shown on invoices and receipts. Max 40 characters |
| **Max Redemptions** | Total redemptions across **all** promotion codes on this coupon. Supply a value — the create request expects one |
| **Daily Redemption Limit** | Optional. Per **customer**, per day. Needs a customer identifier on the payment |
| **Expiry Date** (`redeemBy`) | Optional. After this datetime the coupon can no longer be redeemed |
| **Category Tags** (`appliesTo`) | Optional. Only orders whose `goodsCategory` matches are eligible; empty means all |
| **Metadata** | Free key/value pairs for your own tracking, not shown to customers |

Discount type, discount value and duration are **immutable after creation** — the page warns you before you
submit, and afterwards only **Name** and **Metadata** are editable. **Deactivate** stops further redemptions and
is reversible; **Archive** is irreversible and also deactivates the coupon's promotion codes. Neither touches
discounts already applied. The list shows a lifecycle tag rather than a raw status: archived wins, then expired
(past `redeemBy`), then the coupon's own state.

## How do I issue promotion codes?

| | Single code | Batch |
| - | - | - |
| Code string | You type it. Alphanumeric only, auto-uppercased, unique on your account, **not editable afterwards** | Generated from a **prefix** (alphanumeric, up to 10 characters), e.g. `BFK2X9P`, `BFM7L3Q` |
| Quantity | 1 | 1–10,000 per batch |
| Restrictions | Max Redemptions for this code, expiry, First Time Only, per-currency Minimum Amount | The same set, applied to every code in the batch |

Batch creation is a four-step wizard — pick the coupon, set prefix and quantity, set restrictions, then a
**preview** returning sample codes and an estimated generation time before you commit. Generated codes are
permanent, so plan the prefix and quantity first. The batch runs asynchronously: **Batch Tasks** lists Task ID,
coupon, status (`PENDING` → `PROCESSING` → `SUCCESS` / `FAILED`), success count and creation time, and links
through to the codes a task produced. A code-level expiry cannot extend the coupon — the coupon's own `redeemBy`
is still checked separately, so the earlier of the two ends redemption.

## Why was a code refused at checkout?

Eligibility is decided before the amount is calculated, roughly in this order:

* Coupons are **disabled on your account**, or no coupon configuration exists yet.
* The coupon or the code is inactive, expired, or has exhausted its redemption stock; or the code does not
  belong to the coupon passed in the same request.
* The order amount is below the code's **Minimum Amount** for that currency, or `AMOUNT_OFF` has no amount
  configured for the order currency.
* A per-customer limit, a daily limit or **First Time Only** applies, but the request carried **no customer
  identifier** — the discount is refused rather than applied unrestricted.
* **First Time Only** and the customer already has a completed redemption — scoped to your account as a whole,
  not to this one coupon.
* A `ONCE` coupon was already used, or a `REPEATING` coupon has used up its cycles.

## Where do I see who redeemed what?

The coupon detail page has Overview, Promotion Codes and **Redemption History** tabs. Each redemption row
carries Record ID, promotion code, customer, source type and source ID, **Original / Discount / Final** amount
in the order currency, status and creation time. Filter by customer, source ID, status and date range, then
export the filtered view. Status runs `CREATED` (reserved, not yet final) → `COMPLETED` (confirmed — the only
state analytics counts), or `VOIDED` (the payment never completed, reservation released) / `REVERSED` (rolled
back after a refund).

## How is Discount Analytics calculated?

Three caveats decide whether the numbers match your own. It counts **completed redemptions only**, so `CREATED`,
`VOIDED` and `REVERSED` are excluded. Amounts are **summed across currencies with no conversion**, which is why
the cards and the performance table show bare numbers without a currency symbol — use the redemption export for
per-currency totals. And **Redemption Rate** = completed redemptions ÷ the summed Max Redemptions of the coupons
in scope, so coupons with no cap contribute 0 to the denominator.

Default range is the last 30 days. The trend chart buckets by each redemption's **confirmation** time in your
Portal time zone, and the Daily / Weekly / Monthly toggle re-buckets the same data. Coupon Performance splits
the period per coupon: Total Codes, Redemptions, Total Discount, Redemption Rate, Average Order Value.

## What do the account-level coupon settings do?

| Setting | Effect |
| - | - |
| **Enable Coupons** | Master switch. Off refuses every discount request, whatever the coupon says |
| **Coupon Scope** | `SELF_ONLY` or `PLATFORM_WIDE` — whether discounts are scoped to your own merchant only |
| **Max Active Coupons** | The concurrent-active-coupon ceiling recorded for the account |
| **Max Discount Percent** | Caps `PERCENT_OFF` at redemption: the effective percentage is the **lower** of the coupon's value and this cap. A 60% coupon under a 50% cap discounts 50% — quietly capped, not refused |
| **Refund Reversal Strategy** | See below |

## What happens to a discount on refund, and during a subscription trial?

Under the **Full Reversal** strategy, a **full refund** reverses the redemption — the record becomes `REVERSED`
and the slot returns to both the coupon and the code. A **partial refund** does **not** reverse it: the record
stays `COMPLETED` and the slot stays consumed. A payment that never completed releases the reservation as
`VOIDED` instead. Reversal returns only what was actually consumed, so a cycle that never received a real
discount cannot push a counter negative.

A zero-amount trial period **occupies that period's redemption slot** on the coupon and code, but does **not**
burn the discount's own lifecycle count. A `ONCE` coupon on a subscription with a free trial therefore still has
its single real discount available for the first paid cycle. Outside subscriptions, a zero-amount order with no
discount actually applied consumes nothing.

## How do I apply a discount through the API?

Order create carries a `promotionInfo` object with two mutually exclusive fields, each up to 64 characters.
`promotionInfo.promotionCode` is used by you or by the Waffo-hosted checkout page — the checkout discount input
accepts a **promotion code only**, so a customer can never type a coupon ID. `promotionInfo.couponId` is yours
alone, at order create, to apply a specific coupon with no code involved. Pass one or the other, never both; if
a code and a coupon ID both arrive, the code must belong to that coupon. Field-level rules for order create and
the checkout page's own behaviour are covered in the order-create field guide and the checkout page docs.

## What can I export from the Marketing pages?

Everything the Marketing pages manage is a Portal feature — create, edit, deactivate and archive coupons; create
single codes and code batches; browse redemptions and analytics — and there is no API-key-signed integration
endpoint for any of it. The only API touchpoint is `promotionInfo` on order create, above.

Four lists have an **Export** button: Coupons, Promotion Codes, Redemption History and Discount Analytics. Each
export runs as a task and hands you a CSV named `<type>-<task-id>.csv` — prefix `coupon`, `promotion-code`,
`promotion-record` or `promotion-analytics`. An export honours the filters on screen and covers up to 10,000
rows. Timestamps in lists and exports render in your Portal time zone; the Portal sends its offset with the
request.

***

Numbers not adding up? [Contact support](mailto:support@waffo.com) with the coupon ID, the promotion code, or
the redemption Record ID.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.