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

# Promotions

> Reference for the PromotionsService class in the Tybrite SDK.

The `PromotionsService` class (accessed via `client.promotions`) handles active store discounts, marketing campaigns, and coupon validation logic. Promotions support an `image` field (the default/desktop banner) plus an optional `image_mobile` field for a phone-sized banner — render `image_mobile` on small screens and fall back to `image` everywhere else.

<Tip>
  A merchant's promotions can also be **advertised automatically**. When Google Ads or Meta Ads is connected, Galactic Core can generate a campaign that features a running promotion's real discount — timed to its window — and launch it when the promo goes live and pause it when it ends. This is a merchant-side capability configured in the admin, not an API call. See [Advertising](/advertising).
</Tip>

<Note>
  **Never compute discounts yourself.** Galactic Core calculates the exact discount for
  any cart server-side via `calculatePromotionDiscount` (one promotion) and
  `calculateBestPromotion` (pick the winner). The number you get back always matches what
  the merchant sees in their POS — so you never reimplement `subtotal × percentage`,
  bundle math, or BOGO logic on the storefront. See
  [Calculating a discount](#calculating-a-discount) below.
</Note>

## Methods

### `listPromotions`

Retrieve a list of promotions based on their lifecycle status. This is useful for building a "Deals" or "Coupons" page on your storefront.

```typescript theme={null}
const { promotions, pagination } = await client.promotions.listPromotions({
  status: 'active',
  cartTotal: '120.00',
  limit: 50,
  fields: 'id,name,type,value,min_purchase,image'
});

console.log(`Available promotions: ${promotions.length}`);
```

```json Response theme={null}
{
  "promotions": [
    {
      "id": "df290d89-0400-4b2c-aa56-11fcc486150c",
      "type": "discount",
      "display_type": "Percentage Discount",
      "name": "Spring Sale — 15% Off",
      "value": "15",
      "min_purchase": 50,
      "start_date": "2026-06-19",
      "end_date": "2026-07-20",
      "status": "active",
      "conditions": "Minimum spend $50",
      "has_time_restrictions": false,
      "start_time": null,
      "end_time": null,
      "time_zone": "America/New_York",
      "apply_to_days": null
    },
    {
      "id": "b43fd2cc-622a-454a-965f-18323d11c346",
      "type": "fixed",
      "display_type": "Fixed amount off",
      "name": "$20 Off Orders Over $200",
      "value": "20",
      "min_purchase": 200,
      "start_date": "2026-06-19",
      "end_date": "2026-07-20",
      "status": "active",
      "time_zone": "America/New_York"
    }
  ],
  "pagination": { "limit": 50, "next_cursor": null, "has_more": false }
}
```

**Filter: `cartTotal`**
Providing a `cartTotal` allows the engine to pre-filter promotions that the customer is actually eligible for based on their current shopping bag value (compared against each promotion's `min_purchase`).

***

### `getPromotion`

Fetch full configuration data for a promotion, including its exact discount logic, validity dates, and the products it applies to.

```typescript theme={null}
const promo = await client.promotions.getPromotion({
  id: 'promotion_uuid',
  fields: 'name,type,value,min_purchase,image' // Optional
});

if (promo.image) {
  console.log(`Promotion Banner: ${promo.image}`);
}
```

```json Response theme={null}
{
  "id": "df290d89-0400-4b2c-aa56-11fcc486150c",
  "type": "discount",
  "display_type": "Percentage Discount",
  "name": "Spring Sale — 15% Off",
  "value": "15",
  "min_purchase": 50,
  "start_date": "2026-06-19",
  "end_date": "2026-07-20",
  "status": "active",
  "conditions": "Minimum spend $50",
  "has_time_restrictions": false,
  "start_time": null,
  "end_time": null,
  "time_zone": "America/New_York",
  "apply_to_days": null,
  "image": "https://cdn.example.com/promotions/spring-sale.jpg",
  "image_mobile": "https://cdn.example.com/promotions/spring-sale-mobile.jpg"
}
```

#### Getting the products in a promotion

The `type` field tells you how a promotion works and which product fields to read.

| `type`                            | What to render                                                                                                                                              | Products                                                                                  |
| :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- |
| `discount` (percentage) / `fixed` | A cart-wide discount: show `value` and any `min_purchase`. Use [`calculatePromotionDiscount`](#calculating-a-discount) to get the actual amount off a cart. | **None** — these promotions have no product list.                                         |
| `bundle`                          | A bundle deal.                                                                                                                                              | `bundle_products`: `[{ productId, quantity, discountedPrice }]`                           |
| `bogo`                            | Buy X, get Y free.                                                                                                                                          | `bogo_required_products` (buy) + `bogo_free_products` (free): `[{ productId, quantity }]` |

**The easy way — `expand: 'products'`.** Pass `expand: 'products'` and Galactic Core resolves every product id in the promotion for you, embedding the product details inline. The response gains `bundle_products_resolved`, `bogo_required_products_resolved`, and `bogo_free_products_resolved` — same entries as the id arrays, but each carries a `product` object (or `null` if that product is unavailable). No second lookup:

```typescript theme={null}
const promo = await client.promotions.getPromotion({
  id: 'promotion_uuid',
  expand: 'products',
});

if (promo.type === 'bundle') {
  for (const item of promo.bundle_products_resolved ?? []) {
    if (!item.product) continue; // product no longer available
    console.log(`${item.product.name} ×${item.quantity} — ${item.product.image}`);
    // item.product = { product_id, name, price, image, category }
  }
}

if (promo.type === 'bogo') {
  const buy  = promo.bogo_required_products_resolved ?? [];
  const free = promo.bogo_free_products_resolved ?? [];
  // render "Buy <buy[].product.name>, get <free[].product.name> free"
}
```

```json Response theme={null}
{
  "id": "46e746e2-6ef3-4091-ae4e-6bc8d405a1a2",
  "type": "bundle",
  "display_type": "Bundle promotion",
  "name": "Audio + Wearable Bundle",
  "value": "0",
  "min_purchase": 0,
  "status": "active",
  "time_zone": "America/New_York",
  "bundle_products": [
    { "productId": "7d425c0a-50e7-4e39-af79-6f7945f9dfa8", "quantity": 1, "discountedPrice": 299.99 },
    { "productId": "0186665f-f0a1-4c79-976a-919b5b1c2daa", "quantity": 1, "discountedPrice": 249 }
  ],
  "bundle_products_resolved": [
    {
      "productId": "7d425c0a-50e7-4e39-af79-6f7945f9dfa8",
      "quantity": 1,
      "discountedPrice": 299.99,
      "product": {
        "product_id": "7d425c0a-50e7-4e39-af79-6f7945f9dfa8",
        "name": "Sony WH-1000XM4",
        "price": 349.99,
        "image": "https://cdn.example.com/products/sony-wh1000xm4.jpg",
        "category": "Audio & Headphones"
      }
    }
  ]
}
```

**The manual way.** Without `expand`, the product fields hold product **ids** — fetch each one's full detail with `client.products.getProduct(...)` yourself:

```typescript theme={null}
const promo = await client.promotions.getPromotion({ id: 'promotion_uuid' });
// without expand, products are ids only:
// → promo.bundle_products = [{ productId, quantity, discountedPrice }]

if (promo.type === 'bundle') {
  const items = await Promise.all(
    (promo.bundle_products ?? []).map(async (b) => ({
      ...b,
      product: await client.products.getProduct({ id: b.productId }),
    }))
  );
  // render each item.product with its b.discountedPrice
}
```

For `discount` and `fixed` promotions there are no products to resolve — render `value` / `min_purchase`, and call `calculatePromotionDiscount` (below) to show the customer the exact amount they'll save on their current cart.

***

### Calculating a discount

Use `calculatePromotionDiscount` to compute exactly what a promotion takes off a given cart. **The server does the math**, so the figure you display always matches the merchant's POS — you never multiply a subtotal by a percentage, sum bundle savings, or work out BOGO yourself.

Send the cart you intend to charge. Each line is `{ product_id, quantity, price }`, where `price` is the **unit price** you're charging for that product:

```typescript theme={null}
const result = await client.promotions.calculatePromotionDiscount({
  id: 'promotion_uuid',
  requestBody: {
    cart: [
      { product_id: 'prod_a', quantity: 2, price: 25.00 },
      { product_id: 'prod_b', quantity: 1, price: 40.00 },
    ],
  },
});

if (result.eligible) {
  console.log(`You save $${result.discount}`);   // e.g. 9.00
  const total = 90.00 - result.discount;          // apply it to your cart total
} else {
  console.log(`Not eligible: ${result.reason}`);  // e.g. "Minimum purchase of $120 not met"
}
```

The response is the same shape for every promotion type:

```json theme={null}
{
  "promotion_id": "promotion_uuid",
  "type": "discount",
  "eligible": true,
  "discount": 9.00,
  "reason": null
}
```

* `type` — `discount` (percentage) and `fixed` are cart-wide discounts; `bundle` and `bogo` are computed from the products present in your cart.
* When the cart doesn't qualify, you get `eligible: false`, `discount: 0`, and a `reason` (minimum purchase not met, the promotion is outside its active date/time window, or required products are absent). Show the `reason` to nudge the shopper (e.g. "Add \$30 more to unlock this deal").

This is the right call for **every** type, including percentage and `fixed` — it's how you turn "10% off" into a concrete dollar amount for the current cart.

***

### Auto-applying the best promotion

`calculateBestPromotion` evaluates **all** of the store's active promotions against a cart and returns the single one that saves the customer the most — ideal for auto-applying a deal at checkout without making the shopper hunt for a code:

```typescript theme={null}
const best = await client.promotions.calculateBestPromotion({
  requestBody: {
    cart: [
      { product_id: 'prod_a', quantity: 2, price: 25.00 },
      { product_id: 'prod_b', quantity: 1, price: 40.00 },
    ],
  },
});

if (best.promotion) {
  console.log(`Applied "${best.promotion.name}" — you save $${best.discount}`);
  // best.promotion = { id, name, type }
  const total = 90.00 - best.discount;
} else {
  console.log('No promotions apply to this cart.');
}
```

When nothing applies, `promotion` is `null` and `discount` is `0`:

```json theme={null}
{ "promotion": null, "discount": 0 }
```

***

#### Marketplace storefronts

When a sponsored ad placement or a curated collection hands you a `promotion_id`, pass the **`storeId`** you received alongside it (the `merchant_store_id`). It works the same way across all four calls here — `getPromotion` (including `expand: 'products'`), `calculatePromotionDiscount`, and `calculateBestPromotion`:

```typescript theme={null}
const { merchant_store_id, promotion_id } = placement; // from getAdSlot / getMarketplaceCollection

// Resolve products in one call:
const promo = await client.promotions.getPromotion({
  id: promotion_id,
  storeId: merchant_store_id,
  expand: 'products',
});
// → same Promotion shape as above, scoped to merchant_store_id

// Compute the discount against that merchant's cart:
const result = await client.promotions.calculatePromotionDiscount({
  id: promotion_id,
  storeId: merchant_store_id,
  requestBody: { cart: [{ product_id: 'prod_a', quantity: 1, price: 25.0 }] },
});
// → { promotion_id, type, eligible, discount, reason }

// Or find the best deal among that merchant's promotions:
const best = await client.promotions.calculateBestPromotion({
  storeId: merchant_store_id,
  requestBody: { cart: [{ product_id: 'prod_a', quantity: 1, price: 25.0 }] },
});
// → { promotion: { id, name, type } | null, discount }
```

A marketplace key may read and calculate a single merchant's promotions this way; it cannot list promotions.

***

## Data Schema

### `Promotion` Structure

| Field                                                                                 | Type      | Description                                                                                                                                                                                                                        |
| :------------------------------------------------------------------------------------ | :-------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                                                                                  | `uuid`    | Unique promotion identifier.                                                                                                                                                                                                       |
| `name`                                                                                | `string`  | Promotion display name.                                                                                                                                                                                                            |
| `type`                                                                                | `enum`    | `fixed`, `discount`, `bundle`, `bogo`.                                                                                                                                                                                             |
| `display_type`                                                                        | `string`  | Human-readable label derived from `type` (e.g. "Percentage Discount").                                                                                                                                                             |
| `value`                                                                               | `string`  | The discount amount or percentage, depending on `type`.                                                                                                                                                                            |
| `min_purchase`                                                                        | `number`  | Minimum order total for the promotion to apply.                                                                                                                                                                                    |
| `start_date` / `end_date`                                                             | `date`    | Validity window. Only `active` promotions within this window apply.                                                                                                                                                                |
| `status`                                                                              | `string`  | Lifecycle status.                                                                                                                                                                                                                  |
| `image`                                                                               | `string`  | URL of the promotion banner. This is the default banner — use it on desktop and as the fallback whenever `image_mobile` isn't set.                                                                                                 |
| `image_mobile`                                                                        | `string`  | Optional banner sized for mobile viewports. When present, render it on small screens (e.g. a `<picture>` with a mobile `<source srcset={image_mobile}>` and an `<img src={image}>` fallback); when absent, use `image` everywhere. |
| `bundle_products` / `bogo_required_products` / `bogo_free_products` / `free_products` | `array`   | Product sets (ids) for `bundle` / `bogo` promotions.                                                                                                                                                                               |
| `*_resolved` (e.g. `bundle_products_resolved`)                                        | `array`   | Only present when fetched with `expand: 'products'`. Same entries as the matching id array, each with an embedded `product` (`{ product_id, name, price, image, category }`) or `null`.                                            |
| `has_time_restrictions`                                                               | `boolean` | Whether the promotion only applies on certain days/times (`start_time`, `end_time`, `time_zone`, `apply_to_days`).                                                                                                                 |

### `PromotionCalculation` (returned by `calculatePromotionDiscount`)

| Field          | Type             | Description                                                     |
| :------------- | :--------------- | :-------------------------------------------------------------- |
| `promotion_id` | `uuid`           | The promotion that was evaluated.                               |
| `type`         | `enum`           | `discount`, `fixed`, `bundle`, `bogo`.                          |
| `eligible`     | `boolean`        | Whether the cart qualifies.                                     |
| `discount`     | `number`         | Amount to subtract from the cart total (`0` when not eligible). |
| `reason`       | `string \| null` | Why the cart isn't eligible; `null` when it is.                 |

### `BestPromotion` (returned by `calculateBestPromotion`)

| Field       | Type             | Description                                                              |
| :---------- | :--------------- | :----------------------------------------------------------------------- |
| `promotion` | `object \| null` | The winning promotion `{ id, name, type }`, or `null` when none applies. |
| `discount`  | `number`         | Discount for the winning promotion (`0` when none applies).              |

***

<Tip>
  **Pro Marketing Tip:** Use the `image` field to build high-impact promotional carousels. By associating visual assets directly with your discount logic, you can ensure that your marketing banners always reflect the current campaign status and validity.
</Tip>

***

## Response Codes

Every method here accepts both **publishable** and **secret** keys, and `calculatePromotionDiscount` / `calculateBestPromotion` return `200` (they compute a result, they don't create a resource). Marketplace keys may call `getPromotion`, `calculatePromotionDiscount`, and `calculateBestPromotion` (each with `storeId`) but not `listPromotions`. The `status` query parameter is validated against a strict enum.

| Code  | Meaning                                                                                                                                                                                                                                                              |
| :---- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | Success.                                                                                                                                                                                                                                                             |
| `400` | Invalid `status` enum (must be one of `active`, `inactive`, `scheduled`, `expired`), a malformed query parameter, a calculation request with a missing/invalid `cart` (each line needs `product_id`, `quantity`, `price`), or a marketplace call missing `store_id`. |
| `401` | Invalid or missing API key.                                                                                                                                                                                                                                          |
| `403` | A marketplace key tried to list promotions, or write. Use `getPromotion` / the calculate methods with `store_id` instead.                                                                                                                                            |
| `404` | Promotion not found (`getPromotion` and `calculatePromotionDiscount`). `listPromotions` returns `200` with an empty array; `calculateBestPromotion` returns `200` with `promotion: null` when nothing matches.                                                       |
| `429` | Rate limit exceeded.                                                                                                                                                                                                                                                 |
| `500` | Internal server error.                                                                                                                                                                                                                                               |
