Easy Labs
BillingGuides

Apply a coupon

Validate a customer-entered code and attach the resulting discount to a subscription.

Goal

Take a coupon ID (server-side) or a customer-typed promotion code (e.g. LAUNCH50), validate that it can be redeemed, and attach the resulting discount to a subscription. Coupons can also be scoped to specific products and to a single subscription item rather than the whole subscription.

Prerequisites

  • A sandbox or production API key
  • @easylabs/node installed
  • Either a Coupon you've already created, or a Promotion code that points at one
  • An existing Subscription to apply the discount to

Implementation

1. Create the coupon (one-time setup)

Coupons are reusable — create them once for each campaign or pricing rule. Pick percent_off or amount_off + currency, never both.

import { createClient } from "@easylabs/node";

const easy = await createClient({ apiKey: process.env.EASY_API_KEY! });

const { data: coupon } = await easy.createCoupon({
  duration: "repeating",
  duration_in_months: 3,
  percent_off: 25,
  name: "Launch month — 25% off for 3 months",
  max_redemptions: 500,
});

// Optional: a customer-redeemable code that points at this coupon.
const { data: promo } = await easy.createPromotionCode({
  coupon_id: coupon.id,
  code: "LAUNCH25",
  first_time_only: true,
  minimum_amount: 1000,
  valid_until: "2026-12-31T23:59:59Z",
});

2. Validate a customer-entered code

Always validate before applying — the call returns a discount_preview you can render in your UI ("$12.25 off") and a valid: false reason ("expired", "minimum_not_met") when the code can't be used.

const { data: validation } = await easy.validatePromotionCode({
  code: "LAUNCH25",
  identity_id: "cus_01HXXXXXXXXXXX",
  amount: 4900, // optional — checks minimum_amount if set
});

if (!validation.valid) {
  throw new Error(validation.reason ?? "Invalid promo code");
}

3. Apply the discount to the subscription

Pass exactly one of coupon_id or promotion_code. Optionally scope to a single subscription item by passing subscription_item_id (otherwise the discount applies to the subscription as a whole).

// Apply via promotion code (typical for customer-entered codes)
const { data: discount } = await easy.applySubscriptionDiscount(
  "sub_01HXXXXXXXXXXX",
  { promotion_code: "LAUNCH25" },
);

// Apply via coupon directly (typical for server-side / programmatic discounts)
await easy.applySubscriptionDiscount(
  "sub_01HXXXXXXXXXXX",
  { coupon_id: coupon.id, subscription_item_id: "si_01HXXXXXXXXXXX" },
);

The discount is honored on the next invoice generated by the subscription engine. For repeating coupons, the discount lasts duration_in_months invoices and then drops off automatically.

4. Inspect or remove later

const { data: discounts } = await easy.listSubscriptionDiscounts("sub_01HXXXXXXXXXXX");
await easy.removeSubscriptionDiscount("sub_01HXXXXXXXXXXX", discounts[0].id);

Tradeoffs

  • Coupon vs. promotion code — apply a coupon_id directly when you control the discount logic server-side (anniversary perks, retention saves, internal credits). Use a code when the customer types it in.
  • Subscription-level vs. item-level scope — leaving subscription_item_id unset discounts the whole subscription. Set it to scope to a single line — useful when an add-on is the discounted product but the base plan is not.
  • first_time_only is per-coupon, not per-product — once a customer redeems any code that maps to a given coupon, no other code mapping to that same coupon will validate for them with first_time_only: true.

On this page