> ## 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 sync.

Use the Developer Platform promotions endpoints to manage central promotions, product assignments, and marketplace sync. 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}/sync`       | `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](/api-reference) · Spec: [openapi.json](/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/skipped counts)

## Create promotion

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

### Request body

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

| Field           | Notes                                                 |
| --------------- | ----------------------------------------------------- |
| `discountType`  | `percentage` or `fixed_amount`                        |
| `discountValue` | Positive number; percentage max 100                   |
| `storeKeys`     | At least one connected store key                      |
| `minSpend`      | Optional Lazada voucher minimum spend (defaults to 1) |

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`, `storeKeys`, schedule, `status`, guardrails).

## Assign products

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

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

Assign **does not** push to marketplaces. It creates pending sync rows. Call sync after assign.

```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.

## Sync to marketplaces

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

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

Both fields are optional:

* omit `marketplace` → all promotion marketplaces
* omit `productIds` → all assigned products

Returns `202`:

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

### Marketplace mapping

| Marketplace | Omni push                 |
| ----------- | ------------------------- |
| Shopee      | Product discount campaign |
| Lazada      | Collectible voucher       |
| TikTok      | FIXED\_PRICE activity     |
| Shopify     | Automatic discount        |

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

## 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,
    "storeKeys": ["shopee:singapore: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. Sync
curl -sS -X POST "$BASE/api/v1/promotions/$PROMO_ID/sync" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"marketplace":"shopee"}'
```
