---
title: "Per-SKU gross / contribution P&L"
method: POST
path: "/pnl/skus"
tags: ["P&L"]
---

# Per-SKU gross / contribution P&L

`POST /pnl/skus`

## Request

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

One row per SKU: units sold and the fee lines from the order-level export summed over the window, joined to your cost of goods, with a gross profit and margin.

This is a GROSS / CONTRIBUTION view, not a net bottom line. The fees here are the ones TikTok books against the order itself — referral, affiliate commission, refunds, discounts and the like — and ad spend is NOT among them: TikTok does not attribute ad spend to a single SKU, so `gross_profit` is revenue less those per-SKU fees less COGS, and stops one step short of net earnings. For the true shop-level bottom line that includes ad spend, use `POST /pnl/summary` and `POST /pnl/profit`. If you request the `net_earnings` or `total_expense` columns they are the order-line totals and likewise exclude ad spend.

`gross_profit` is an upper bound while `units_uncosted` is non-zero — uncosted units are counted, never assumed free. Windowed by order PAID date; single shop only.

## Request body

- PnlSkusRequest: POST /pnl/skus request body — per-SKU gross / contribution P&L.
  - `start_date` string, date, nullable: Inclusive paid-date window start (YYYY-MM-DD).
  - `end_date` string, date, nullable: Inclusive paid-date window end (YYYY-MM-DD).
  - `product_id` string, nullable: Filter to one product.
  - `sku_id` string, nullable: Filter to one SKU.
  - `limit` integer: Page size (SKUs per page).
  - `offset` integer: Page offset.
  - `amounts_fields` union: Which fee columns each SKU's `amounts` carries, as window SUMS. `standard` (default) is the 12 lines a P&L waterfall needs; `full` is every column TikTok ships; or pass an explicit list of field names. Unknown names are rejected rather than ignored, so a typo cannot read as a zero fee. The resolved list comes back as `selected_fields`. Note: gross_profit is computed from a fixed internal set, so this selection changes only what is echoed, never the profit math.
    - 'standard' | 'full'
    - string[]
  - `sort` 'gross_sales' | 'gross_profit' | 'units': Sort key, always descending, tie-broken by product_id then sku_id so paging is stable. `gross_sales` (default) surfaces top sellers, `gross_profit` top contributors, `units` highest volume.

## Response `200`

Successful Response

- PnlSkusResponse: Per-SKU gross / contribution P&L for one shop.
  - `shop_id` integer, required
  - `seller_id` string, required
  - `currency` string, required
  - `start_date` string, required
  - `end_date` string, required
  - `total` integer, required: Distinct SKUs matching the window and filters, before paging.
  - `limit` integer, required
  - `offset` integer, required
  - `selected_fields` string[]: The fee columns every SKU's `amounts` carries, in canonical order — provenance for the projection you asked for, so an absent key reads as 'not requested' rather than 'no data'.
  - `currency_mismatched_costs` integer, required: COGS rows skipped because they are priced in a different currency than the shop. Non-zero means some SKUs' COGS is silently absent, so their `gross_profit` is overstated and `units_uncosted` under-counts.
  - `skus` PnlSkuRollup[], required
    - `product_id` string, nullable
    - `sku_id` string, nullable
    - `sku_name` string, nullable
    - `product_name` string, nullable
    - `units` integer, required: Units sold on this SKU's order lines in the window.
    - `amounts` object: Window SUMS of the requested fee columns for this SKU, keyed by TikTok's own column names — the same projection as `selected_fields`. Signs are TikTok's: gross sales positive, cost lines negative. `null` means TikTok sent no value for that column; an absent key means you did not request it. The `net_earnings` and `total_expense` columns, when requested, are the ORDER-LINE totals and therefore EXCLUDE ad spend — they are not this SKU's true net.
    - `cogs` number, required: Cost of goods on units sold this window. Negative, or 0.0 when no cost is on file.
    - `gross_profit` number, required: Contribution profit: the net earnings TikTok booked on this SKU's order lines, less COGS. EXCLUDES ad spend, so it is a gross / contribution figure, not the true bottom line. An UPPER BOUND while `units_uncosted` is non-zero, since uncosted units are counted as free rather than guessed at.
    - `gross_margin_pct` number, nullable: gross_profit as a percentage of gross sales. Null when gross sales is 0.
    - `units_costed` integer, required
    - `units_uncosted` integer, required: Units with no COGS on file. Fill these in via PUT /pnl/costs; gross_profit is an upper bound while this is non-zero.
    - `currency` string, required
  - `data_status` PnlDataStatus, required: Freshness and reachability of this shop's P&L data. ``state`` distinguishes "no sales" from "we cannot read the finance export": ``no_permission`` means the shop's Seller Center account lacks finance-tab access and the fix is customer-side, not a retry.
    - `state` union, required: Latest export-run status: never_run | pending | parsed | empty | failed | no_permission.
      - 'never_run' | 'pending' | 'parsed' | 'empty' | 'failed' | 'no_permission'
      - string
    - `last_synced_at` string, nullable: ISO timestamp of the newest successfully parsed run.
    - `last_window` PnlDateWindow: The date window an export run requested.
      - `start_date` string, nullable
      - `end_date` string, nullable

## Other responses

- `422`: Validation Error

## Changes

> 76 revisions in range; 1 not diffed.

- **2026-09-19** `291230805105`: 1 breaking, 1 info
  - the `data_status/state` response's property type changed from `string` to no type for status `200`
  - added `subschema #1, subschema #2` to the `data_status/state` response property `anyOf` list for the response status `200`
- **2026-09-11** `02635188ace8`: 1 info
  - endpoint added

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