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

# Promotions API

> Manage central promotions, product assignments, and marketplace delivery.

Use the Developer Platform promotions endpoints to manage central promotions, product assignments, and marketplace delivery. Semantics match the workspace Promotions UI (`/app/{organizationId}/promotions`).

## Authentication

Send an OmniCommerce API key or OAuth client-credentials token:

```http theme={null}
Authorization: Bearer $OMNI_API_KEY
Content-Type: application/json
```

| Endpoint | Required scope |
| - | - |
| `GET /api/v1/promotions` | `promotions:read` |
| `GET /api/v1/promotions/{promotionId}` | `promotions:read` |
| `GET /api/v1/promotions/{promotionId}/products` | `promotions:read` |
| `POST /api/v1/promotions` | `promotions:write` |
| `PATCH /api/v1/promotions/{promotionId}` | `promotions:write` |
| `DELETE /api/v1/promotions/{promotionId}` | `promotions:write` |
| `POST /api/v1/promotions/{promotionId}/products` | `promotions:write` |
| `DELETE /api/v1/promotions/{promotionId}/products` | `promotions:write` |
| `POST /api/v1/promotions/{promotionId}/retry` | `promotions:write` |

OAuth clients must include the scope on the client. API keys created from organization settings receive all developer scopes by default.

Session-authenticated calls must include `organizationId` in the query (GET) or JSON body (writes).

OpenAPI reference: [API reference](https://docs.omnicommerce.sg/api-reference.md) · Spec: [openapi.json](https://docs.omnicommerce.sg/openapi.json)

## List promotions

```http theme={null}
GET /api/v1/promotions
```

### Query parameters

| Parameter | Description |
| - | - |
| `organizationId` | Organization scope for session auth. Optional for single-org OAuth/API keys. |
| `q` | Search promotion name. |
| `status` | Effective status: `all`, `draft`, `scheduled`, `active`, `paused`, `ended`, `archived`. |
| `marketplace` | Comma-separated marketplaces (`shopee,lazada,tiktok,shopify`). |
| `store` | Comma-separated store keys (`marketplace:country:storeId`). |
| `sortBy` | `createdAt` (default), `name`, `startsAt`, `status`. |
| `sortDir` | `asc` or `desc` (default). |
| `offset` | Zero-based offset. Default `0`. |
| `limit` | Page size. Default `20`, max `50`. |

### Response

```json theme={null}
{
  "items": [],
  "hasMore": false,
  "nextOffset": 20,
  "summary": {
    "totalCount": 0,
    "statusCounts": {}
  },
  "pagination": {
    "offset": 0,
    "limit": 20,
    "total": 0,
    "hasMore": false,
    "nextOffset": null,
    "maxLimit": 50,
    "defaultLimit": 20
  }
}
```

Each list item includes promotion fields plus:

* `effectiveStatus` — status resolved from schedule windows
* `syncMeta` — aggregated marketplace sync (`lastSyncedAt`, pending/failed/synced/applied/skipped counts). `appliedCount` includes only confirmed rows with a marketplace campaign reference.

## Create promotion

```http theme={null}
POST /api/v1/promotions
```

### Request body

```json theme={null}
{
  "name": "Christmas",
  "discountType": "fixed_amount",
  "discountValue": 12,
  "targetStores": [{ "marketplace": "shopee", "country": "singapore", "storeId": "115085776" }],
  "startsAt": "2026-07-06T17:00:00.000Z",
  "endsAt": "2026-08-02T03:59:00.000Z"
}
```

| Field | Notes |
| - | - |
| `discountType` | `percentage` or `fixed_amount` |
| `discountValue` | Nonnegative number; percentage max 100; use zero for gift-only offers |
| `targetStores` | 1–100 connected stores as `{ marketplace, country, storeId }` |
| `storeDiscounts` | Optional discounts for individual target stores; other stores use shared terms |
| `minSpend` | Optional Lazada voucher minimum spend (defaults to 1) |

For `gift_with_purchase`, set `discountValue` to `0` for a gift/sample-only
offer or provide a positive value for Lazada Combo Buy. Use
`mechanicConfig.giftType` (`gift` or `sample`), `giftQuantity`, the purchase
threshold, and `gifts[]`. Lazada maps these to Flexicombo. Its eight supported
wire modes are money off, percentage off, free gift, free sample, and each
discount type combined with either a gift or sample.

Free `buy_x_get_y` benefits also map to Lazada Flexicombo. Percentage- or
fixed-discount benefit SKUs do not: Lazada Combo Buy discounts the qualifying
order rather than the benefit SKU, so OmniCommerce reports that store as failed
instead of changing the promotion's meaning.

New promotions default to active; a future start date displays as scheduled. Pass `status: "draft"` to save without activation. Explicit paused, ended, and archived states remain inactive.

Returns `201` with `{ "promotion": { ... } }`.

## Get / update / delete

```http theme={null}
GET    /api/v1/promotions/{promotionId}
PATCH  /api/v1/promotions/{promotionId}
DELETE /api/v1/promotions/{promotionId}
```

Update accepts partial fields (`name`, `discountType`, `discountValue`, `targetStores`, `storeDiscounts`, schedule, `status`, guardrails). Partial `mechanicConfig` updates preserve its other fields.

To change only one store's discount, use its exact connected store key:

```json theme={null}
{
  "storeDiscounts": [
    {
      "storeKey": "lazada:singapore:1758352057",
      "discountType": "percentage",
      "discountValue": 18
    }
  ]
}
```

This preserves the shared discount and every other store's override. Set an
entry's `discountValue` to `null` to return that store to shared terms. A store
must already be a campaign target (or be included in the same `targetStores`
update). An empty array makes no changes. At most 100 distinct store keys may
be supplied. Fixed amounts use the campaign's currency.

The assistant's `manage_promotions` update also accepts `removeProductIds`
with the terms and schedule. It commits these changes together for each
campaign and queues marketplace synchronization after the commit. Use
`promotionIds` for shared changes or `updates[]` for different changes per
campaign. Each batch supports up to 50 campaigns and reports individual
successes and failures.

`bundle_deal` represents a quantity-based buy-together discount on assigned
products; it does not create a combined product listing. Eligible quantities
may include repeated units of the same product. An exact “one A plus one B”
requirement needs a marketplace mechanic that enforces those distinct items.

## Assign products

```http theme={null}
POST /api/v1/promotions/{promotionId}/products
```

```json theme={null}
{
  "productIds": ["f831911c-ba05-4c83-9a03-e026e0aaf102"],
  "priority": 0
}
```

Assigning products to an active or scheduled promotion delivers them to the marketplaces automatically. A draft promotion delivers when you activate it.

`productDiscounts` is optional. Each entry sets that product's own percentage or fixed amount, using the campaign discount type. `discountValue: null` returns the product to the shared campaign discount. Omit a product to leave its current discount unchanged. Only product discounts and flash sales accept this. Shopee and TikTok publish the resulting promotional price for each product. Lazada vouchers and Shopify discounts use one amount for every product, so sync fails for those stores while any product has its own discount. A product's own discount replaces any store discount for that product. Changing the promotion's discount type, or changing it to a mechanic without a price, is rejected until those product discounts are cleared.

```json theme={null}
{
  "productIds": ["f831911c-ba05-4c83-9a03-e026e0aaf102"],
  "productDiscounts": [
    {
      "productId": "f831911c-ba05-4c83-9a03-e026e0aaf102",
      "discountValue": 5
    }
  ]
}
```

```http theme={null}
GET /api/v1/promotions/{promotionId}/products
```

Lists current assignments.

```http theme={null}
DELETE /api/v1/promotions/{promotionId}/products
```

```json theme={null}
{
  "productIds": ["f831911c-ba05-4c83-9a03-e026e0aaf102"]
}
```

Unassigns products and queues marketplace teardown.

## Marketplace delivery

Promotions reach the marketplaces automatically. You never need a separate sync call:

* Creating a promotion as `active` or `scheduled`, or changing its `status` to either, delivers it.
* Editing an active or scheduled promotion, or assigning or removing products, delivers the change.
* Changing `status` to `paused`, `ended`, or `archived` removes it from the marketplaces.

Check the result in the promotion's `syncMeta` and delivery status.

### Retry a failed delivery

```http theme={null}
POST /api/v1/promotions/{promotionId}/retry
```

```json theme={null}
{
  "marketplace": "shopee"
}
```

Use this after a delivery failed, for example once a store is reconnected or a product is published. It brings each marketplace back in line with the promotion's current status. Both fields are optional:

* `marketplace`: retry one marketplace. Omit it to retry every promotion marketplace.
* `productIds`: retry only these products. Omit it to retry every assigned product.

Returns `202`:

```json theme={null}
{
  "ok": true,
  "queued": true,
  "syncBatchId": "…",
  "marketplaces": ["shopee"],
  "jobId": "…"
}
```

### Marketplace mapping

| Marketplace | Native or mapped writes |
| - | - |
| Shopee | Seller discounts, product/Live vouchers, Bundle Deals |
| Lazada | Store Flash Sales, seller vouchers, and all Flexicombo gift/sample/discount modes |
| TikTok | Product Discount, Flash Deal, Gift with Purchase, Buy More Save More activities |
| Shopify | Automatic/code discounts, code and automatic BXGY, minimum-quantity bundle discounts |

Products only sync to marketplaces they are published on. Other target rows are marked `skipped`.

Imported Shopee seller vouchers are managed in Shopee Seller Centre. Importing promotions reads supported product-specific voucher codes, eligibility, minimum spend, caps, and schedules; repeat imports refresh these terms. These imported vouchers cannot be edited or pushed back from Omni. Shop-wide, affiliate, cashback, and unsupported restricted offers are excluded. Voucher redemption does not replace a product's sale price.

Marketplace selection expresses intent, not proof of delivery. Check the promotion detail delivery status after it is delivered. `Applied` requires a successful sync row with a marketplace campaign ID; unsupported, read-only, permission, validation, and partial-failure outcomes retain the exact reason.

## Example flow

```bash theme={null}
# 1. Create
curl -sS -X POST "$BASE/api/v1/promotions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Weekend 10%",
    "discountType": "percentage",
    "discountValue": 10,
    "targetStores": [{ "marketplace": "shopee", "country": "singapore", "storeId": "115085776" }]
  }'

# 2. Assign products
curl -sS -X POST "$BASE/api/v1/promotions/$PROMO_ID/products" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"productIds":["…"]}'

# 3. Activate: this delivers the promotion to the marketplaces
curl -sS -X PATCH "$BASE/api/v1/promotions/$PROMO_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status":"active"}'
```


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