---
title: "Campaign prep: price, stock, COGS, fees and break-even ROAS per SKU"
method: POST
path: "/product-insights/campaign-prep"
tags: ["Products"]
---

# Campaign prep: price, stock, COGS, fees and break-even ROAS per SKU

`POST /product-insights/campaign-prep`

## Request

- Base URL: `https://api.reacherapp.com/public/v1`
- URL: `https://api.reacherapp.com/public/v1/product-insights/campaign-prep`
- Auth: API key in header `x-api-key` and API key in header `x-shop-id`

Prepares one shop's products for a TikTok platform campaign or flash sale. Per SKU: current price, warehouse stock, cost of goods, TikTok fees, the margin left for ad spend and the break-even ROAS = selling price / (selling price - COGS - fees), where the selling price is the price less ``discount_pct`` (rounded to the cent) and fees are ``fee_rate`` of it. Read-only: it registers nothing with TikTok.

COGS comes from the shop's stored costs; SKUs without one fall back to this request's ``cogs`` rows, then to ``margin_pct`` (``cogs_source``); ``cogs`` rows not applied to any SKU are listed in ``caller_cogs_unused``. Fees are measured from the shop's P&L order lines over ``fee_window_days`` (TikTok's own total expense per line — referral, affiliate commission, shipping — as a share of net sales, on the SKU or else across its product; ad spend is not included), or taken from ``fee_rate`` when given (``fees_source``). Nothing missing is assumed: ``break_even_roas`` is null and ``status`` says why — ``no_price`` (a multi-variant product whose only current price is its range), ``no_cogs`` (no stored, caller or margin COGS), ``no_fee_data`` (no usable P&L fee data and no ``fee_rate``) or ``no_break_even`` (costs meet or exceed the selling price). ``missing_inputs`` lists every missing input, and the ``skus_with_*`` counts say how much of ``skus_scanned`` each input covers. Single-shop only.

## Request body

- CampaignPrepRequest: Body for ``/product-insights/campaign-prep``.
  - `product_ids` string[], nullable: Restrict to these TikTok product ids; every listed product when omitted.
  - `discount_pct` number: The campaign discount being considered, in percent of the current price (0-90). The discounted price is rounded half-up to the cent.
  - `fee_rate` number, nullable: Override the observed fee rate: TikTok fees as a fraction of the selling price (e.g. 0.18). When omitted, the rate is measured from the shop's P&L order lines over ``fee_window_days``; a SKU whose product has no P&L lines then gets no break-even.
  - `fee_window_days` integer: Trailing days of P&L order lines the fee rate is measured over, ending yesterday.
  - `cogs` CallerCogs[], nullable: Unit costs for SKUs the shop has stored no COGS for; a stored cost always wins. A row with ``sku_id`` beats one for the whole product.
    - `product_id` string, required
    - `sku_id` string, nullable
    - `amount` number, required: Cost of one unit.
    - `currency` string, nullable: ISO currency of ``amount``; assumed to be the catalog's when omitted. A row in another currency is never converted and is reported in ``caller_cogs_unused``.
  - `margin_pct` number, nullable: Shop-wide gross margin in percent of the pre-discount price, used as COGS = price x (1 - margin_pct/100) for SKUs with neither a stored nor a ``cogs`` row.
  - `page` integer: 1-based page over the SKUs.
  - `page_size` integer: SKUs per page.

## Response `200`

Successful Response

- CampaignPrepResponse
  - `shop_id` integer, required
  - `discount_pct` number, required
  - `cogs_as_of` string, required: The date COGS was resolved for (YYYY-MM-DD).
  - `fee_window_start` string, nullable
  - `fee_window_end` string, nullable
  - `currency_mismatched_costs` integer, required: The shop's stored COGS rows skipped because their currency differs from the catalog's; they are never converted.
  - `skus_scanned` integer, required
  - `skus_with_price` integer, required
  - `skus_with_cogs` integer, required
  - `skus_with_cogs_by_source` CogsSourceCounts, required
    - `stored` integer, required
    - `caller` integer, required
  - `caller_cogs_unused` CallerCogsUnused[]: Rows of the request's ``cogs`` that were not applied to any SKU.
    - `product_id` string, required
    - `sku_id` string, nullable
    - `reason` 'unmatched' | 'currency_mismatch' | 'superseded', required: ``unmatched``: no SKU in this scan has that id. ``currency_mismatch``: the row's currency is not the catalog's. ``superseded``: every SKU it covers already had a stored cost or a SKU-level ``cogs`` row.
  - `skus_with_fees` integer, required
  - `skus_with_break_even` integer, required
  - `data` CampaignPrepSku[], required
    - `product_id` string, required
    - `sku_id` string, required
    - `seller_sku` string, nullable
    - `product_name` string, required
    - `currency` string, required
    - `inventory` integer, nullable: Warehouse stock on the last catalog sync.
    - `inventory_updated_at` string, nullable: When the catalog sync last wrote this SKU (ISO 8601 UTC).
    - `price` number, nullable: Current price of one unit, before the campaign discount.
    - `price_source` 'product' | 'product_range', nullable: ``product``: the product has one current price for every SKU. ``product_range``: the product's variants are priced differently and only the range is current, so ``price`` is null.
    - `product_price_min` number, nullable
    - `product_price_max` number, nullable
    - `stored_sku_price` number, nullable: The SKU's last stored price, for reference only: it stopped refreshing on 2026-07-09 and is never used for the break-even.
    - `discount_amount` number, nullable
    - `selling_price` number, nullable: ``price`` less ``discount_pct``, rounded half-up to the cent.
    - `cogs` number, nullable: Cost of goods per unit; ``cogs_source`` says where it came from.
    - `cogs_source` 'stored' | 'caller' | 'none', required: ``stored``: the shop's stored costs, in force today. ``caller``: this request's ``cogs`` or ``margin_pct``. ``none``: no COGS, so no break-even.
    - `fee_rate` number, nullable: TikTok's per-order deductions (referral, affiliate commission, shipping, ...) as a fraction of ``selling_price``; never ad spend.
    - `fees_source` 'pnl' | 'override' | 'none', required
    - `fee_basis` 'sku' | 'product', nullable: Whether a P&L fee rate was measured on this SKU or across its product's SKUs.
    - `fee_observed_units` integer, nullable: Units sold in the window the P&L fee rate was measured over.
    - `fees` number, nullable: Fees per unit at ``selling_price``.
    - `margin_per_unit` number, nullable: ``selling_price`` - ``cogs`` - ``fees``: what is left for ad spend. Computed from unrounded COGS and fees, so it can differ from the shown parts by a cent.
    - `break_even_roas` number, nullable: ``selling_price`` / ``margin_per_unit``: the ROAS at which ad spend uses up the margin, counting what the buyer pays (the discounted price) as revenue. Null whenever ``status`` is not ``ok``.
    - `status` 'ok' | 'no_price' | 'no_cogs' | 'no_fee_data' | 'no_break_even', required
    - `missing_inputs` string[]
  - `pagination` PublicApiCorePaginationPaginationMeta, required: Pagination metadata returned in responses.
    - `total_count` integer, required
    - `page` integer, required
    - `page_size` integer, required
    - `total_pages` integer, required

## Other responses

- `422`: Validation Error

## Changes

> 76 revisions in range; 1 not diffed.

- **2026-10-20** `19bd89c85d71`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/reacherapp/apis/reacher-data-api/changes/product-insights/campaign-prep/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)
