---
title: "Create a promotion activity and attach its products"
method: POST
path: "/promotions/activities"
tags: ["Promotions"]
---

# Create a promotion activity and attach its products

`POST /promotions/activities`

## Request

- Base URL: `https://api.reacherapp.com/public/v1`
- URL: `https://api.reacherapp.com/public/v1/promotions/activities`
- Auth: API key in header `x-api-key` and API key in header `x-shop-id`

Creates a FLASHSALE, FIXED_PRICE or DIRECT_DISCOUNT activity on the shop's TikTok account and attaches the products (or SKUs at VARIATION level) in batches of at most 300. The activity id is recorded the moment TikTok returns it, so a lost answer or a failed attach never creates a second activity: the response `state` says how far it got and `retry-attach` finishes it.

TikTok requires `begin_time` in the future and refuses a title it already has. Coupons cannot be created through the API.

Requires `read_write` scope, the shop's TikTok grant with `seller.promotion.write`, and an `Idempotency-Key` (the same key with the same body replays; a different body is 409). `X-Dry-Run: true` returns the create and attach bodies that would be sent without calling TikTok.

## Headers

- `Idempotency-Key` string, nullable
- `X-Dry-Run` string, nullable

## Request body

- PromotionActivityCreateRequest: POST /promotions/activities — a flash sale, fixed-price or direct-discount activity with its products. Validated as one so a bad product never reaches TikTok's create call; products are attached in batches of at most 300 items after the activity exists.
  - `title` string, required
  - `activity_type` 'FLASHSALE' | 'FIXED_PRICE' | 'DIRECT_DISCOUNT', required
  - `product_level` 'PRODUCT' | 'VARIATION', required
  - `begin_time` integer, required: Unix seconds; TikTok requires the future.
  - `end_time` integer, required: Unix seconds.
  - `participation_limit` 'BUYER_NO_LIMIT' | 'BUYER_LIMIT_ONLY_ONE'
  - `products` ActivityProductIn[], required
    - `id` string, required
    - `activity_price_amount` union
      - number
      - string
    - `discount` integer, nullable
    - `quantity_limit` integer
    - `quantity_per_user` integer
    - `skus` ActivitySkuIn[]
      - `id` string, required
      - `activity_price_amount` union
        - number
        - string
      - `discount` integer, nullable: Percent off, integer.
      - `quantity_limit` integer: 1..99, or -1 for unlimited.
      - `quantity_per_user` integer: 1..99, or -1 for unlimited.

## Response `200`

The `Idempotency-Key` was already processed (`replayed: true`); a dry run answers 200 with `{dry_run, would}` instead.

- PromotionActivityWriteResponse: What a create, retry or state-changing write returns: the record as it stands now.
  - `data` PromotionActivityRecordOut, required: Reacher's record of an activity it created: the saga state beside TikTok's status.
    - `activity_id` string, nullable: TikTok's id; null only while the create is in flight.
    - `idempotency_key` string, required
    - `state` 'creating' | 'attach_pending' | 'attach_failed' | 'ready' | 'needs_reconciliation' | 'deactivated' | 'failed', required: creating: no id yet · attach_pending: id known, products not all attached (retry) · attach_failed: TikTok rejected an attach (fix and retry) · ready: every product attached · needs_reconciliation: TikTok's answer was lost twice; check Seller Center · deactivated · failed.
    - `tiktok_status` string, nullable: TikTok's status when last seen (NOT_START, ONGOING, EXPIRED, DEACTIVATED).
    - `title` string, required
    - `activity_type` string, required
    - `product_level` string, required
    - `begin_time` string, required: ISO timestamp (UTC).
    - `end_time` string, required: ISO timestamp (UTC).
    - `product_count` integer, required: Products in the create request.
    - `attached_items` string[], required: Items TikTok has confirmed on the activity: product ids, or `product_id:sku_id` at VARIATION level. Kept in step by attach, reprice and remove calls made through Reacher.
    - `last_error` string, nullable
    - `created_at` string, required
    - `updated_at` string, required
  - `replayed` boolean: True when this Idempotency-Key had already been processed.

## Other responses

- `201`: Successful Response
- `400`: Validation error, multi-shop key, missing `Idempotency-Key`, or TikTok rejected the request (`PROMOTION_REJECTED`; `PROMOTION_CONFLICT`, `FLASH_SALE_ONGOING`, `PROMOTION_PERIOD_TOO_LONG` for those causes).
- `403`: API key lacks `read_write` scope, or the shop's TikTok grant lacks the promotion scopes (`SCOPE_NOT_GRANTED`).
- `404`: No such activity for this shop, or TikTok does not allow this authorization to access it.
- `409`: `Idempotency-Key` reused with a different body (`IDEMPOTENCY_CONFLICT`), another request is writing to this activity (`CONCURRENT_WRITE`), the record is not in a resumable state (`NOT_RESUMABLE`), or the shop is not connected (`SHOP_NOT_CONNECTED`).
- `422`: Validation Error
- `429`: Write-tier rate limit, or TikTok is rate limiting this shop (`UPSTREAM_RATE_LIMITED`, with `Retry-After`).
- `502`: TikTok's answer was lost and a re-read could not confirm the change (`UPSTREAM_UNCERTAIN`), or TikTok did not complete the request (`UPSTREAM_ERROR`). For a UK shop, `UPSTREAM_ERROR` with `details.reason` and `Retry-After` means its customer-service token could not be renewed in time; retry with the same key.
- `503`: The write limiter (Valkey) is unavailable; no write was attempted.

## Changes

> 76 revisions in range; 1 not diffed.

- **2026-10-25** `84094c04c9fd`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/reacherapp/apis/reacher-data-api/changes/promotions/activities/post.md)

---

[API](https://skmtc.dev/reacherapp/apis/reacher-data-api.md) - [All operations](https://skmtc.dev/reacherapp/apis/reacher-data-api/llms.txt) - [OpenAPI document](https://skmtc.dev/reacherapp/apis/reacher-data-api/revisions/63ce74b30b5a?raw)
