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

# Discounts & Promotions

> Create coupons and promotion codes, and let customers use them at checkout

## What You Can Do

* Create **coupons**: a percentage or a fixed amount off, limited to some products, a total number of uses and an expiry, with a code customers type at checkout
* Hand out more **promotion codes** for the same coupon, each with its own customer restrictions — eligible emails, uses per customer, a minimum amount — one per influencer or campaign channel, or a batch of single-use codes
* Let customers type a code on the checkout page, share a product link with a code already applied, or apply a coupon or pre-fill a promotion code from your server when you create a checkout session

> [!NOTE]
>
> Discounts apply to **one-time products, new subscriptions and plan changes**. On a subscription, the coupon's duration decides how many billing periods are discounted — see [Subscriptions](#subscriptions). On a plan change, a new code replaces the subscription's discount, and without one the discount carries over while it can — see [Plan Changes](#plan-changes).

***

## Coupons and Promotion Codes

A **coupon** defines the discount and has its own code customers can type — set it when you create the coupon, or let Pancake generate one. A coupon only limits its expiry and total uses, so its own code works for any customer within those limits. A **promotion code** is another code for the same coupon that carries customer restrictions: a coupon can have many, and a customer must pass the promotion code's restrictions as well as the coupon's expiry and total uses. You can create promotion codes together with the coupon, in the same request, or add them later. A coupon you apply from your server with `couponId` needs no code.

| Rule | Set on | Description |
| - | - | - |
| Discount | Coupon | Percentage off, or a fixed amount per currency. A fixed amount does not apply to an order whose currency is not listed — there is no currency conversion |
| Products | Coupon | Limit the coupon to some products, or leave it store-wide |
| Expiry | Coupon, promotion code | After the expiry the code can no longer be used. A promotion code cannot outlive its coupon |
| Total uses | Coupon, promotion code | Uses across all customers. The coupon's limit counts its own code and all its promotion codes; a promotion code's limit cannot exceed its coupon's |
| Eligible customers | Promotion code | Only these customer emails (case-insensitive) |
| Uses per customer | Promotion code | Lifetime uses of the code per customer |
| Minimum amount | Promotion code | Minimum per currency, before discount and tax. Currencies not listed have no minimum |

Coupons and their codes belong to one store and one environment: a code created in test mode does not work in live mode. Codes are case-insensitive — a customer can type `spring-20` for `SPRING-20`.

***

## Creating Coupons

### Via Dashboard

Go to **Dashboard → Coupons** to create a coupon, add promotion codes, generate a batch of codes, and see how many times each code has been used — see [Coupons](https://docs.waffo.ai/dashboard/discounts.md) in the Dashboard guide.

### Via API or SDK

Create coupons and promotion codes from your server with the [Coupon](https://docs.waffo.ai/api-reference/endpoints/coupons/overview.md) and [Promotion Code](https://docs.waffo.ai/api-reference/endpoints/promotion-codes/overview.md) endpoints:

```typescript TypeScript theme={"system"}
import { CouponDiscountType, WaffoPancake } from "@waffo/pancake-ts";

const client = new WaffoPancake({
  merchantId: process.env.WAFFO_MERCHANT_ID!,
  privateKey: process.env.WAFFO_PRIVATE_KEY!,
});

const { coupon } = await client.coupons.create(
  {
    storeId: "STO_xxx",
    name: "Spring sale",
    discount: { type: CouponDiscountType.Percentage, percentOff: 20 },
    maxRedemptions: 500,
    code: "SPRING-20", // the coupon's own code; omit it for a random one
    // Promotion codes created together with the coupon (up to 20), each with its own restrictions
    promotionCodes: [{ code: "SPRING-NEW", restrictions: { perCustomerLimit: 1 } }],
  },
  { idempotencyKey: "coupon-spring-2026" },
);

// A code for one influencer: 100 uses, at least $50 before tax
await client.promotionCodes.create({
  couponId: coupon.id,
  code: "KOL-ALICE",
  restrictions: { maxRedemptions: 100, minimumAmount: { USD: "50.00" } },
});
```

```go Go theme={"system"}
res, err := client.Coupons.Create(ctx, pancake.CreateCouponParams{
    StoreID:        "STO_xxx",
    Name:           "Spring sale",
    Discount:       pancake.CouponDiscount{Type: pancake.CouponDiscountTypePercentage, PercentOff: pancake.Ptr(20.0)},
    MaxRedemptions: pancake.Ptr(500),
    Code:           pancake.Ptr("SPRING-20"),
    PromotionCodes: []pancake.CreateCouponPromotionCodeParams{{
        Code:         pancake.Ptr("SPRING-NEW"),
        Restrictions: &pancake.PromotionCodeRestrictions{PerCustomerLimit: pancake.Ptr(1)},
    }},
}, pancake.WithIdempotencyKey("coupon-spring-2026"))
```

## Common Campaigns

What to set up for the promotions merchants run most often. Each one is a coupon, plus promotion codes where customers need different rules — set up in the [Dashboard](https://docs.waffo.ai/dashboard/discounts.md) or with the SDK.

> **Launch sale: a public code for a limited time**
>
> A 20% code everyone can use for two weeks, with a cap so the campaign cannot run away.
>
> * **Set up**: one coupon — percentage 20, expiry in two weeks, redemption limit (for example 500), all products or just the launch product. Its own code is the public code, for example `LAUNCH-20`.
> * **Customers get it**: they type the code on the checkout page — your product link shows the code entry field as soon as the coupon exists. For ads and emails, put the code in the link so nothing has to be typed: `…/product/{productId}?discount_code=LAUNCH-20`.
> * **Watch out**: the expiry is checked at the moment of entry and again at order time, so a customer who opens the page before the deadline and pays after it is charged the full price. A coupon's expiry also ends the discount on subscriptions it was applied to.
>
> ```typescript theme={"system"}
> await client.coupons.create({
>   storeId: "STO_xxx",
>   name: "Launch sale",
>   code: "LAUNCH-20",
>   discount: { type: CouponDiscountType.Percentage, percentOff: 20 },
>   maxRedemptions: 500,
>   redeemBy: "2026-11-15T00:00:00.000Z",
> });
> ```

> **Influencers and channels: one code per partner**
>
> The same discount, but you want to know which partner each order came from and cap each of them separately.
>
> * **Set up**: one coupon with the discount, then one promotion code per partner on it — `KOL-ALICE`, `KOL-BOB`, `PODCAST-10` — each with its own total uses and, if the deal needs it, a minimum order amount. The coupon's own code can stay unpublished.
> * **Customers get it**: the partner shares their code or a product link with `?discount_code=KOL-ALICE`.
> * **Track**: the coupon details list uses per code; export the codes as CSV for your partner report. Deactivate a partner's code on its own when the deal ends — the other codes keep working.
>
> ```typescript theme={"system"}
> await client.promotionCodes.create({
>   couponId: "CPN_xxx",
>   code: "KOL-ALICE",
>   restrictions: { maxRedemptions: 200, minimumAmount: { USD: "30.00" } },
> });
> ```

> **Newsletter or giveaway: one single-use code per person**
>
> Each recipient gets their own code that works once, so codes cannot be shared.
>
> * **Set up**: one coupon, then **Generate in bulk** on it — the number of codes you need (up to 500 per batch), a prefix such as `NEWS-`, and *each code can be used up to 1 time*. Download the CSV and merge it into your email tool.
> * **Customers get it**: each email carries one code; the customer types it at checkout.
> * **Watch out**: a code is used up by a paid order; an order left unpaid holds the use for 72 hours, then the code becomes usable again. Bulk codes are random, so there is no way to tell who used which code except by matching the CSV to the orders' emails.
>
> ```typescript theme={"system"}
> const { promotionCodes } = await client.promotionCodes.generate({
>   couponId: "CPN_xxx",
>   count: 500,
>   prefix: "NEWS-",
>   restrictions: { maxRedemptions: 1 },
> });
> ```

> **Win-back or VIP list: only certain customers**
>
> A discount only the customers on your list can use, even if the code leaks.
>
> * **Set up**: a promotion code with *Only specific customers can use it* — paste the emails — and *Each customer can use it up to 1 time*.
> * **Customers get it**: they type the code at checkout. The code is matched against the email they enter on the checkout page, so the page asks for the email first when the code is applied before one is typed; a customer who is not on the list sees that the code is for specific customers.
> * **Watch out**: the match is on the exact email (case-insensitive). A customer who checks out with a different email than the one you have is refused — tell them which address the offer was sent to.
>
> ```typescript theme={"system"}
> await client.promotionCodes.create({
>   couponId: "CPN_xxx",
>   code: "COMEBACK-30",
>   restrictions: { customerEmails: ["jane.doe@example.com"], perCustomerLimit: 1 },
> });
> ```

> **Loyalty tiers or partner pricing: you decide, no code shown**
>
> Your own system knows who gets the discount — a membership tier, a reseller, a quote you agreed on — and the customer should not see or share a code.
>
> * **Set up**: a coupon per tier (`Gold 15%`, `Partner 25%`), with no need to publish its code.
> * **Customers get it**: your server creates the checkout session with `couponId` and sends the customer to its `checkoutUrl`. The checkout page shows the discount and the discounted total, no code and no code entry field; the customer cannot remove it.
> * **Watch out**: `couponId` needs your API Key, so decide the eligibility in your server code — never from values the browser sends. With the Next.js SDK, `createCheckoutAction` strips `couponId`; call `@waffo/pancake-ts` from your own server code instead. To let the customer stack their own code on top, use a promotion code with `promotionCode` plus `allowPromotionCodes: true` instead — a coupon cannot be combined with code entry.
>
> ```typescript theme={"system"}
> // In your server code, after checking the customer's tier
> const session = await client.checkout.createSession({
>   storeId: "STO_xxx",
>   productId: "PROD_xxx",
>   currency: "USD",
>   couponId: tier === "gold" ? "CPN_gold" : undefined,
>   buyerEmail: customer.email,
> });
> redirect(session.checkoutUrl);
> ```

> **Subscriptions: first months cheaper, or a reason to switch to annual**
>
> "25% off for 3 months" on a monthly plan, or a code that only works on the annual plan to move customers over.
>
> * **Set up**: for the first months, a coupon with duration *Multiple billing cycles* = 3. For the annual push, a coupon limited to the annual product, so the code is refused on the monthly plan with `PRODUCT_NOT_ELIGIBLE`.
> * **Customers get it**: new subscribers type the code at checkout. Existing monthly subscribers switch from the Customer Portal: the plan change page shows the code entry field when a code can be used on the annual plan, or you create the plan change session yourself with `promotionCode` and send them the link.
> * **Watch out**: a free trial does not use the discount — it starts with the first paid period. A paid trial does, and counts as the first period. On a plan change, a new code replaces the current discount; without one the current discount carries over with the periods already used. See [Subscriptions](#subscriptions) and [Plan Changes](#plan-changes).
>
> ```typescript theme={"system"}
> await client.coupons.create({
>   storeId: "STO_xxx",
>   name: "Annual upgrade",
>   code: "GO-ANNUAL",
>   discount: { type: CouponDiscountType.Percentage, percentOff: 25 },
>   duration: CouponDuration.Repeating,
>   durationInPeriods: 3,
>   productIds: ["PROD_annual"],
> });
> ```

> **Test first, then go live**
>
> Coupons belong to one environment, so a coupon built in test mode is not visible in live mode.
>
> * **Set up**: build the coupon and its codes in test mode and run a test checkout — the test checkout accepts test cards.
> * **Go live**: open the coupon and choose **Copy to live** (products must be published to live first). The copy starts with 0 uses; active, unexpired promotion codes come along if you tick the option. With the API, create the coupon again with your live API Key.
> * **Watch out**: a code already in use on live stops the copy — deactivate it there first, or copy without codes.

***

## Applying Discounts at Checkout

A discount reaches the checkout page in one of four ways. The customer always sees the discounted, tax-inclusive total before paying.

| Way | How | Can the customer change it? |
| - | - | - |
| Code entry field | The customer types a coupon code or promotion code on the checkout page | Yes — they can replace or remove it |
| Product link with a code | Add `discount_code` to a product link | No — the code is applied and locked |
| Coupon set by your server | Pass `couponId` when you create a checkout session with merchant credentials (API Key or JWT) | No — the discount is applied and locked |
| Promotion code set by your server | Pass `promotionCode` when you create a checkout session with merchant credentials (API Key or JWT) | Only if you also pass `allowPromotionCodes: true` — then they can replace or remove it |

### Code Entry Field

Product links and your store page show the code entry field when the store has a code that can be used on the product: an `active` coupon that covers the product, is not expired and is under its total usage limit, with its own code or a promotion code that is `active`, not expired and under its own usage limit. Rules that depend on the customer, such as eligible customers or a minimum amount, are checked only when the code is entered. A code that has just expired or reached its limit can keep the field showing for up to 5 minutes. Checkout sessions you create with your API Key follow the same rule when you leave out `allowPromotionCodes`; pass `true` to always show the field, or `false` to hide it:

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: "PROD_xxx",
  currency: "USD",
  allowPromotionCodes: true,
});
```

#### Scenarios

How the checkout page decides, and what to pass, in common situations:

| Scenario | Code entry field | What to pass |
| - | - | - |
| You created a coupon in the Dashboard for all products; a customer opens a product link or your store page | Shown | Nothing — the page detects the coupon |
| The coupon is limited to product A; the customer opens product B | Hidden on B, shown on A | Nothing |
| The store's only coupon has expired, is used up, or was deactivated | Hidden (within 5 minutes) | Nothing — create a new coupon to show it again |
| The coupon's only code is limited to specific customer emails | Shown — customer rules are not part of the check. Another customer's code is refused with `CUSTOMER_NOT_ELIGIBLE`; applied before an email is entered, the page asks for the email first | Nothing |
| The only coupon is a fixed amount in USD; the customer checks out in EUR | Shown — currencies are not part of the check. The code is refused with `CURRENCY_MISMATCH` | Add a EUR amount to a new coupon, or leave it |
| You create the session from your server with your API Key | Hidden, even when the store has codes | `allowPromotionCodes: true` to show it |
| You create the session from your server and want a code pre-filled | Code shown as applied and locked | `promotionCode: "KOL-ALICE"`; add `allowPromotionCodes: true` to let the customer replace or remove it |
| You decide the discount yourself (loyalty tier, partner deal) and do not want a code shown | Hidden; the discount is applied and locked, with no code | `couponId: "CPN_xxx"` — cannot be combined with `promotionCode` or `allowPromotionCodes` |
| You send a campaign link with the code built in | Hidden; the code is applied and locked. A code that cannot be used is explained, with **Continue without discount** | `?discount_code=SPRING-20` on the product link |
| Your own storefront creates the session with Store Slug (no merchant credentials) | Default detection, as for a product link | `allowPromotionCodes: true` or `false` to override the detection |
| A customer changes plan from the Customer Portal | Shown when a code can be used on the new plan's product; without a code, the current discount carries over while it can | Nothing — or `couponId` / `promotionCode` on a plan change session you create |
| You use the Next.js SDK's `createCheckoutAction` | Hidden unless the button passes `allowPromotionCodes` | `allowPromotionCodes` on `<CheckoutButton>`; `couponId` and `promotionCode` are stripped there — set them in server code with `@waffo/pancake-ts` |

### Product Link with a Code

Add `discount_code` to a [product link](https://docs.waffo.ai/checkout/checkout-flow.md#product-purchase-links). The code is applied when the page opens, and the code entry field is hidden:

```
https://pancake.waffo.ai/store/{store-slug}/product/{productId}?currency=USD&discount_code=SPRING-20
```

If the code cannot be used — it has expired, or it does not cover the product — the checkout page tells the customer why.

### Coupon Set by Your Server

Pass `couponId` (the coupon's `CPN_` ID) when you create the checkout session. The customer sees the discount — its name, amount and duration — but not a code, even when the coupon has its own code; to show a code, pre-fill it with `promotionCode` instead. The coupon is checked right away: a coupon from another store or environment, a deactivated or expired coupon, a product the coupon does not cover, or a fixed amount with no value in the session currency is rejected with `409` — see [Create Checkout Session](https://docs.waffo.ai/api-reference/endpoints/orders/create-checkout-session.md#discounts).

```typescript theme={"system"}
const session = await client.checkout.authenticated.create({
  productId: "PROD_xxx",
  currency: "USD",
  buyerIdentity: "userIdInYourSystem",
  couponId: "CPN_xxx",
});
```

`couponId` cannot be combined with `allowPromotionCodes` or `promotionCode` — sending both returns `400`. `couponId` is accepted only with merchant credentials (API Key or JWT): a Store Slug request or a customer session token gets `403`, and the [Next.js SDK](https://docs.waffo.ai/integrate/nextjs.md#discounts) server action strips it, so customers cannot pick a coupon for themselves.

### Promotion Code Set by Your Server

Pass `promotionCode` — the text of a coupon code or promotion code, case-insensitive — to apply it when the checkout page opens. It is checked right away against the same rules as `couponId`, and a code that cannot be used is rejected with `409`. Whether the customer can change it is up to `allowPromotionCodes`:

| `allowPromotionCodes` | On the checkout page |
| - | - |
| `true` | The code is applied, and the customer can replace it with another code or remove it. After removing it they can enter a code again |
| Omitted or `false` | The code is applied and locked |

```typescript theme={"system"}
const session = await client.checkout.createSession({
  productId: "PROD_xxx",
  currency: "USD",
  promotionCode: "KOL-ALICE",
  allowPromotionCodes: true, // let the customer replace or remove it
});
```

Like `couponId`, `promotionCode` is accepted only with merchant credentials (API Key or JWT) — a Store Slug request or a customer session token gets `403`, and the Next.js SDK server action strips it.

***

## When a Code Cannot Be Used

Pancake checks the code when the customer enters it, and makes the final decision when they place the order. The check at entry is a preview, so a few rules are left to the order:

* For a guest checkout, only paid orders count toward the code's total uses: the code is refused at entry only once its paid uses reach the limit. Unpaid orders and the per-customer limit are checked when the order is placed — if other customers' unpaid orders hold the remaining uses, the order is refused then.
* For a signed-in customer, their own unpaid order with the same code does not count against the use limits — except an unpaid plan change that still holds the use: one already confirmed and waiting for its switch date, or one the current discount is being carried over to.
* A customer who leaves the payment page without paying can come back and enter the same code again. When they place the new order, it takes over the use held by their unpaid order.

The checkout page shows the reason in the customer's language:

| Reason | Meaning |
| - | - |
| `CODE_NOT_FOUND` | No active code with this text in this store and environment. A deactivated code, or a code from another store, shows the same reason |
| `CODE_INACTIVE` | The coupon or promotion code is no longer active — for example, it was deactivated after the customer applied it |
| `CODE_EXPIRED` | The code has expired. A code within 60 seconds of its expiry already counts as expired — when the checkout session is created, when the code is entered and when the order is placed |
| `MAX_REACHED` | The code has reached its total number of uses |
| `USER_LIMIT_REACHED` | The customer has used the code as often as allowed |
| `CUSTOMER_NOT_ELIGIBLE` | The customer's email is not on the code's list. A code limited to specific customers can only be checked against an email: when the customer applies it before entering one, the checkout page asks for the email first |
| `PRODUCT_NOT_ELIGIBLE` | The code does not cover this product |
| `CURRENCY_MISMATCH` | The fixed amount has no value in the checkout currency |
| `MINIMUM_NOT_MET` | The order is below the code's minimum amount (before discount and tax); the minimum is shown |
| `TEMPORARILY_UNAVAILABLE` | Reserved — not returned at present. A customer who enters codes too quickly gets HTTP `429` instead, and can try again shortly |

If a rule fails only when the order is placed — for example, someone else took the last use a moment earlier — no order is created, nothing is charged and the checkout session stays usable. What the customer can do next depends on how the code was applied:

| Applied by | What the customer can do |
| - | - |
| Code entry field | Remove the code and pay the full price, or try another code |
| Product link with a code | The code is locked: reopen the product link without `discount_code`. When a link's code is rejected as the page opens, that page offers **Continue without discount** |
| Coupon set by your server | The coupon is locked: create a new checkout session without `couponId` |
| Promotion code set by your server | With `allowPromotionCodes: true`, as for the code entry field. Otherwise the code is locked: create a new checkout session without `promotionCode` |

A customer who placed an order with a code but has not paid can place a new order with the same code. The new order takes over the use held by the earlier one; the earlier order stays payable, but if it is paid as well it is flagged for review and not counted as another use.

An unpaid order holds its use for 72 hours. After that, the next order that uses the same coupon — with any of its promotion codes, placed by any customer — takes the use back. The unpaid order is not canceled: it keeps its status, and if it is paid later it is flagged for review and not counted as another use.

***

## Subscriptions

New subscriptions take a discount the same ways as one-time products, with the same rules. Before paying, the customer sees the first charge and what each later period costs, with and without the discount.

### How Many Periods Are Discounted

| `duration` | Discounted billing periods |
| - | - |
| `once` | One period |
| `repeating` | `durationInPeriods` periods in a row |
| `forever` | Every period, until the coupon's expiry |

Only a period whose price before the discount is above zero uses up the discount. A 100% discount still uses up its period, even though nothing is charged.

### Trials

| Trial | What is discounted |
| - | - |
| No trial | The first charge, then the following periods |
| Free trial | Nothing during the trial: it costs nothing and uses no period. The discount starts with the first paid period |
| Paid trial | The trial charge is discounted and counts as the first period — a `once` coupon is used up by the trial, and a `repeating` coupon counts its periods from the trial |

A promotion code's minimum amount is checked against the first charge, before the discount and tax: the trial price for a paid trial, the regular price otherwise. Renewals are not checked again.

For example, at $10 a month with a 20% coupon for 3 periods (`repeating`), periods 1 to 3 charge $8 and period 4 onward $10. With a 7-day free trial and a `once` coupon, the trial costs $0, the first paid month is discounted and later months are full price.

### When a Use Is Counted

* A use is counted once, when the first discounted charge succeeds: the first paid charge, or the trial charge for a paid trial. Renewals do not count again.
* Until then the subscription holds the use. If the subscription ends before that charge — canceled during a free trial, or its first payment never succeeds — the use is given back and the customer can use the code again. A subscription order left unpaid for more than 72 hours is not canceled, but it no longer holds the use: the next order that uses the same coupon, with any of its promotion codes, takes it back.
* If the coupon expires before that charge and the customer is charged the full price, the use is given back as well.
* A new order by the same customer with the same code takes over the use only from an earlier order that was never paid. An active subscription in its free trial keeps its hold: the new order does not take it over, and the held use counts toward the code's limits.

### When the Discount Ends

* When the duration runs out, renewals are charged the full price.
* **The coupon's expiry also ends discounts already on subscriptions**: renewals after it are charged the full price, even with `forever`. A promotion code's own expiry only stops new uses.
* Deactivating a coupon does not end discounts already on subscriptions until their next plan change — see [Plan Changes](#plan-changes).
* Canceling a subscription ends its discount for good. Undoing the cancellation before the period ends keeps the discount.
* A plan change ends the discount, unless it carries over to the new plan — see [Plan Changes](#plan-changes).

### Plan Changes

A plan change replaces the subscription with a new one on the new plan. The plan change page shows a code entry field when the plan change session allows it, and you can set `couponId` or `promotionCode` on a plan change session the same way as on a new purchase — see [Create Checkout Session](https://docs.waffo.ai/api-reference/endpoints/orders/create-checkout-session.md#discounts). A product link's `discount_code` does not apply to plan changes. The plan change page does not ask for an email, so a promotion code's eligible-customer list is matched against the original subscription's customer email — both when the code is entered and when the plan change is submitted.

| The plan change | The discount on the new subscription |
| - | - |
| Comes with a code — entered on the plan change page, or set with `couponId` or `promotionCode` | The new discount replaces the current one, and what is left of the current discount is dropped. It is a new use of the code: every rule is checked again when the change is submitted, it holds a use and is counted once, and its periods count from the new subscription. Entering the code the subscription already has is a new use as well |
| Comes without a code, and the current discount can continue | The current discount carries over. Periods already used are not reset — the remaining periods continue on the new plan. Carrying over is not a new use: no use is held or counted |
| Comes without a code, and the current discount cannot continue | The new subscription has no discount. A discount cannot continue once its periods are used up, once the coupon has expired or expires within 60 seconds, or once the coupon has been deactivated. |

So **deactivating a coupon ends the discounts on subscriptions at their next plan change.**

* **The first period.** On a change that takes effect immediately, the new subscription's first period costs the new plan's price minus the credit for the unused part of the current period — or the amount you set with `changeAmount` or `changeCreditAmount`. When that is above zero, the discount applies to it and it uses up one period. **The discount is worked out on the new plan's full price, and then the credit is taken off**: a percentage coupon takes its percentage of the new plan's price, a fixed amount stays the same, and the discount is capped at the first period's price, so the customer pays the first period's price minus the discount, never below zero. A first period whose discount covers it in full still uses up one period. When it is zero — usually a downgrade whose credit covers the new plan — it takes no discount and uses no period, and the discount starts with the second period. A change that takes effect next period starts with a regular-price period, and a change that grants a trial follows the trial rules above.
* **The credit is based on what the customer pays.** The credit for the unused part of the current period is worked out from what that period costs the customer: the discounted amount when the period is discounted, the list price otherwise.
* **A promotion code's minimum amount** is checked against the new plan's regular price, before the discount and tax. The credit is not taken off.
* **When a use is counted.** A new code is counted when the first discounted charge of the new subscription succeeds — the first period, or the second period when the first is zero. If the change does not go through — the customer does not finish it, the payment channel reports it failed, or the change scheduled for next period is canceled — the use is given back. A change that is started but never completes stops holding its use after 72 hours, and the next order that uses the same coupon, with any of its promotion codes, takes it back — the same goes for a use being carried over to it from a subscription that has since ended. When the current subscription's own code is still waiting for its first discounted charge, a discount that carries over takes that held use along to the new subscription; a new code or no code gives it back.

For example, at $5 a month with $1 off for 3 periods (`repeating`), 1 of them used, the customer upgrades to $10 a month halfway through the period without a code. The credit is $2 — half of the $4 they paid — so the first period is $8 before the discount and $7 to pay, the next period is $9, and later periods are $10. If they enter a new 20% `once` code instead, the discount is $2 — 20% of the $10 plan — so the first period is $8 before the discount and $6 to pay, and later periods are $10: the old discount's remaining period is dropped.

### Amounts on Subscription Payments

* The subscription order keeps the list price. Each payment records what Pancake expects for that period: the original price, the discount and the amount to pay. The [`subscription.payment_succeeded`](https://docs.waffo.ai/api-reference/webhooks) webhook carries them in `listPrice` (`listTotal`, `discountAmount`, `total`).
* A charge that does not match the expected discount — the discount ended early, or the customer paid more or less — is reconciled by Pancake internally. The payment keeps the expected amounts; what was actually taken is in `chargedAmount` on the webhook.
* The first receipt — for the trial charge of a paid trial, or for the first charge without a trial — lists the original price, the discount, the subtotal, the tax and the amount paid when that charge is discounted. A free trial's first receipt is for \$0 and shows no breakdown.
* Each renewal receipt shows the same breakdown when its period is discounted. The first charge after a trial sends no renewal receipt — with a free trial, that is the first discounted period. The customer sees its expected amount in the upcoming-charge reminder.
* The next charge shown in emails includes the discount only when that period is still discounted.
* After a plan change, the new subscription's first payment expects the first period's amounts (see [Plan Changes](#plan-changes)), but no receipt is sent for it. When the first period is zero, this first payment is a \$0 payment; later payments expect the regular-period amounts. The customer gets a plan-changed email instead, which shows the new plan's regular price before any discount, the next billing date, and the next charge amount — discounted when that period still gets the discount. It doesn't show the first period's amount or a discount breakdown. The payment record keeps the first period's amounts. Later renewals get receipts as described above.

***

## Amounts, Receipts and Usage

* **The discount applies to the tax-inclusive price.** A percentage discount is rounded down to the currency's smallest unit; a fixed amount never takes the total below zero, and a 100% discount produces a free order.
* **Orders, payments, receipts and the Dashboard show three amounts**: the original price, the discount, and what the customer paid. Refunds are capped at what the customer paid.
* **An order's price snapshot keeps the list price.** Its `total` is the tax-inclusive price before the discount, for one-time and subscription orders alike. The discounted amount of each charge is on the payment (`snapshotAmountDetails.total`).
* **A discounted receipt** lists the original price, the discount, the subtotal (after the discount, excluding tax), the tax and the amount paid. The subtotal and tax lines appear only when tax applies.
* **A use is counted when the payment succeeds** — for a subscription, the first discounted charge (see [Subscriptions](#subscriptions)). A canceled or expired order gives the use back, so the customer can use the code again. A refund does not give the use back.

***

## Next Steps

> **[Coupon Endpoints](https://docs.waffo.ai/api-reference/endpoints/coupons/overview.md)**
>
> Create, rename and deactivate coupons

> **[Create Checkout Session](https://docs.waffo.ai/api-reference/endpoints/orders/create-checkout-session.md#discounts)**
>
> Apply a coupon or show the code entry field


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