# Promotion Codes

Make coupons and promotion codes, let customers apply them at checkout, on a plan change or on a credit pack, and apply a discount to a workspace yourself.

A promotion code is a Stripe coupon your customers can type. The coupon says what comes off (a percentage, or a fixed amount in one currency) and for how long; the code is the text a customer enters to get it. Both live in your own Stripe account, so a coupon you make in the Stripe dashboard works the same as one you make in the console.

> **Note:**
  Promotion codes need an SDK release with promotion code support. On an older
  server the SDK sends the purchase again without the code and says so
  (`promotionCodeDropped`), and the customer can still type the code on Stripe's
  page.

## Making a code

> **Note:**

- Stripe connected ([Connect Stripe](/billing-setup/connect-stripe)): coupons and codes are made in your Stripe account.
- A console role with `stripe_coupons`.

[Open coupons in the console](https://console.buildbase.app/dashboard/admin/subscriptions/coupons)

### 1. Open Coupons

In the **Billing** menu choose **Coupons**. The page lists **Coupons** (what comes off) and **Promotion codes** (what customers type). Press **New coupon**.

![Billing > Coupons with empty Coupons and Promotion codes lists, and the New coupon button highlighted as step 1](/images/billing/promotion-codes/console-coupons.webp)

### 2. Make the coupon

Fill **Name** (it is shown on the customer's invoice) and choose the **Discount**: **A percentage off** with **Percent off**, or **A fixed amount off** with **Amount off** and its **Currency**. **How long** is **The first invoice only**, **A number of months** with **Months**, or **Every invoice**. Under **Applies to plans**, tick plans to limit the coupon to them; with none ticked it takes off any purchase. **Redemption limit** and **Redeemable until** are optional. Press **Create coupon**.

![The New coupon dialog: Name Launch week, A percentage off, Percent off 20, How long A number of months highlighted as step 2, Months 3, Pro ticked under Applies to plans, Redemption limit 100, and the Create coupon button](/images/billing/promotion-codes/console-new-coupon.webp)

### 3. Give it a code

Press **New promotion code**. Choose the **Coupon** and type the **Code** customers will enter, such as `LAUNCH20`; codes are matched without regard to case. Optionally set a **Redemption limit**, an **Expires** date, **First purchase only**, or a **Minimum order** **In** a currency. Press **Create code**.

![The New promotion code dialog: Coupon Launch week (20% off), Code LAUNCH20 highlighted as step 3, Redemption limit 50, First purchase only switched on, and the Create code button](/images/billing/promotion-codes/console-new-code.webp)

### 4. Taking a code or a coupon off

A code is archived: press **Archive** on its row, then **Archive code**. It stops working at checkout, on plan changes and on credit packs, and the discounts it already gave are kept.

![Archive this code?: LAUNCH20 stops working at checkout, on plan changes and on credit packs, discounts it already gave are kept, and the Archive code button highlighted as step 4](/images/billing/promotion-codes/console-archive-code.webp)

A coupon is deleted rather than archived (**Delete**, then **Delete coupon**). A subscription that already holds it keeps its discount after it is deleted; nobody new can redeem it, and its codes stop working.

### Check that it worked

The coupon is listed as **Redeemable**, with what it takes off, how long, its redemptions and the plans it applies to, and the code as **Active** with its coupon and restrictions.

![Billing > Coupons: Launch week, Redeemable, 20% off for 3 months, 0 of 100 redeemed, on Pro; and LAUNCH20, Active, on Launch week, 0 of 50 redeemed, First purchase](/images/billing/promotion-codes/console-coupons-after.webp)

Coupons made in the Stripe dashboard are listed too, marked **Made outside BuildBase**.

## Where a customer can use it

| Where                  | How                                                                             |
| ---------------------- | ------------------------------------------------------------------------------- |
| A link to your pricing | `?promo=LAUNCH20` (or `?coupon=`) on a page with `<PricingPage>`                |
| The plan picker        | The code field next to the interval tabs prices every plan with the code        |
| Stripe's checkout page | The code box, as before, whenever your app has not applied a code itself        |
| Your own form          | `usePromotionCode()` checks a code, then pass `promotionCode` with the purchase |

A code from a link carries through sign-in: the customer picks a plan on your pricing page, signs in, and the plan picker opens with the code applied.

> **Note:**

`promotion` on the `PricingPage` render prop, and its `promotionCode` prop: **Requires `@buildbase/sdk` 1.0.0 or later.**

```tsx
import { PricingPage } from '@buildbase/sdk/react';

export default function Pricing() {
  return (
    <PricingPage
      slug="main-pricing"
      redirectBaseUrl="https://app.example.com/dashboard"
    >
      {({ plans, selectPlan, promotion }) => (
        <>
          {promotion.code && <p>Code {promotion.code} will be applied.</p>}
          {plans.map((plan) => (
            <button
              key={plan._id}
              onClick={() => selectPlan(plan._id, 'monthly', 'usd')}
            >
              {plan.name}
            </button>
          ))}
        </>
      )}
    </PricingPage>
  );
}
```

Turn the field in the plan picker off with `ui.settings.subscription.promoCode: false`.

## Checking a code before anyone pays

`usePromotionCode()` asks the server what a code takes off one plan or credit pack. It needs no sign-in, so a public pricing page can show the discounted price. A code that does not exist is an answer (`valid: false`), not an error.

> **Note:**

`usePromotionCode` and `priceWithPromotionCode`: **Requires `@buildbase/sdk` 1.0.0 or later.**

```tsx
import {
  formatMinorAmountIntl,
  priceWithPromotionCode,
  usePromotionCode,
} from '@buildbase/sdk/react';

function PromoField({ planVersionId, price }) {
  const [code, setCode] = useState('');
  const { check, data, loading } = usePromotionCode();
  const after = priceWithPromotionCode(data, price, 'usd');

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        check(code, {
          planVersionId,
          billingInterval: 'monthly',
          currency: 'usd',
        });
      }}
    >
      <input value={code} onChange={(e) => setCode(e.target.value)} />
      <button disabled={loading}>Apply</button>
      {data && !data.valid && <p>That code is not valid.</p>}
      {after != null && <p>Now {formatMinorAmountIntl(after, 'usd')}</p>}
    </form>
  );
}
```

The answer holds the coupon, its restrictions, and the plan's price with and without the code (`amount.list`, `amount.discount`, `amount.total`, in the currency's smallest unit). `priceWithPromotionCode` prices any other plan from the same answer, the way Stripe would; it returns `null` where only Stripe can tell, and the server decides when the customer buys.

The check is rate limited per visitor, because codes are guessable. Check once when the customer presses Apply, not on every keystroke.

## Applying a code to a purchase

Pass `promotionCode` with the purchase:

```tsx
const { createCheckoutSession } = useCreateCheckoutSession(workspaceId);
await createCheckoutSession({
  planVersionId,
  billingInterval: 'monthly',
  stripeOptions: { promotionCode: 'LAUNCH20' },
});

const { updateSubscription } = useUpdateSubscription(workspaceId);
await updateSubscription(planVersionId, { promotionCode: 'LAUNCH20' });

const { purchaseCredits } = usePurchaseCredits(workspaceId);
await purchaseCredits({
  creditPackageId,
  successUrl,
  cancelUrl,
  promotionCode: 'LAUNCH20',
});
```

`getCheckoutStripeParams` on the provider can return a `promotionCode` too. From your server, use `subscription.checkout`, `subscription.update` and `credits.purchase` with the same field.

- **At checkout** the code is applied to the Stripe session, and the box on Stripe's page is turned off for that session (Stripe takes one or the other).
- **On a plan change** the code goes with the change, so the prorated invoice is discounted. It replaces a code the customer applied earlier; a coupon you applied stays.
- **On a credit pack**, a code that makes the pack free completes with no payment, and the pack is granted. The code and the amount it took off are recorded on the credits.

## When a code is refused

A code is checked before anything is created in Stripe. A code that cannot be used answers `400` with `code: 'PROMOTION_CODE_INVALID'` and a `reason`:

| `reason`            | What it means                                                                                           |
| ------------------- | ------------------------------------------------------------------------------------------------------- |
| `not_found`         | No active code has this text, or its coupon can no longer be redeemed                                   |
| `currency_mismatch` | The coupon takes a fixed amount off in another currency                                                 |
| `not_applicable`    | The coupon is limited to products this purchase does not include                                        |
| `refused_by_stripe` | Stripe's own rule: a first-purchase code for a returning customer, a minimum amount, a redemption limit |

A first-purchase-only code is judged by Stripe per Stripe customer, and each workspace is its own customer. The check reports the restriction (`restrictions.firstTimeTransaction`) rather than judging it.

## Applying a discount to a workspace yourself

Open the workspace in the console, then **Apply discount** on its subscription. Pick a coupon or type a code, and give a reason; the reason is kept on the subscription's history. The new discount sits beside the ones the subscription already holds unless you tick **Replace them**. **Remove discount** takes one off. Both need create on `stripe_subscription_actions`, as every item of the **Actions** menu does; the Coupons screen has its own permission, `stripe_coupons`.

Either way, the subscription screen your customers see lists each discount under the plan's price, with how long it lasts.

## Webhooks and events

- `subscription.discount_applied` and `subscription.discount_removed` fire for every discount a subscription gains or loses, whoever made the change: a customer's code, an admin, the Stripe dashboard, or a discount running out. A one-time code given at checkout reduces only the first invoice and Stripe never lists it on the subscription, so it sends `subscription.discount_applied` and no `subscription.discount_removed`; that announcement needs `customer.discount.created` on your Stripe webhook endpoint, which **Check Stripe setup** adds.
- `subscription.created` and `payment.succeeded` carry `couponId` and `promotionCode` when a code was involved, and `subscription.upgraded` and `subscription.downgraded` carry the `promotionCode` sent with the change.
- The plan picker reports `promo_code_applied` (with the coupon id) and `promo_code_rejected` (with the reason only, never what was typed) to your analytics.
