---
title: "Contribution profit (P&L less your costs)"
method: POST
path: "/pnl/profit"
tags: ["P&L"]
---

# Contribution profit (P&L less your costs)

`POST /pnl/profit`

TikTok's bottom line carried down by the costs it cannot see. ``net_earnings`` is taken from the daily series unchanged — the only source that includes GMV Max ad spend — and cost of goods and sample cost are subtracted from it. Set ``cadence`` to get the running series as well as the totals.

Units with no cost on file are COUNTED, never assumed to be zero, so ``contribution_profit`` is an upper bound while ``units_uncosted`` or ``sample_units_uncosted`` is non-zero. Use ``POST /pnl/costs/coverage`` to get the list of what is missing.

## Request body

- PnlProfitRequest — POST /pnl/profit request body — TikTok's P&L less your own costs.
  - `start_date` string, date, nullable — Inclusive window start (YYYY-MM-DD).
  - `end_date` string, date, nullable — Inclusive window end (YYYY-MM-DD).
  - `cadence` 'total' | 'day' | 'week' | 'biweekly' | 'month' — `total` returns window totals only. Anything else also returns a bucketed series — the running P&L with costs applied.
  - `sample_basis` 'shipped' | 'approved' | 'requested' — Which sample milestone the cost is charged against. `shipped` is when the unit physically leaves; use `approved` or `requested` if you accrue earlier.

## Response `200`

Successful Response

- PnlProfitResponse — Contribution profit for one shop.
  - `shop_id` integer, required
  - `seller_id` string, required
  - `currency` string, required
  - `start_date` string, required
  - `end_date` string, required
  - `cadence` string, required
  - `sample_basis` string, required
  - `days_with_data` integer, required
  - `totals` PnlProfitLines, required — TikTok's bottom line carried down to a contribution profit. Cost lines are negative like the rest of the P&L waterfall, so the three lines above ``contribution_profit`` sum to it.
    - `net_earnings` number, required — TikTok's own bottom line for the window, from the daily series — the only source that includes GMV Max ad spend.
    - `cogs` number, required — Cost of goods on units sold. Negative, or 0.0 when unknown.
    - `sample_cost` number, required — Cost of samples sent. Negative, or 0.0 when unknown.
    - `contribution_profit` number, required — net_earnings + cogs + sample_cost. An UPPER BOUND whenever units_uncosted or sample_units_uncosted is non-zero — those units are counted as free, not guessed at.
    - `units` integer, required — Units on order lines in the window.
    - `units_costed` integer, required
    - `units_uncosted` integer, required — Units with no COGS on file. Fill these in via PUT /pnl/costs.
    - `units_on_refunded_lines` integer, required — Units sitting on a line that carries a refund. A PARTIAL refund flags the whole line, so this is an upper bound on units returned — it is reported, never netted out, because whether a refunded unit still costs you is your accounting policy, not ours.
    - `sample_units` integer, required — Sample sends in the window, from Reacher's own sample funnel. The Seller Center P&L export cannot supply this: free samples are not orders and never appear in it.
    - `sample_units_costed` integer, required
    - `sample_units_uncosted` integer, required
  - `series` PnlProfitPoint[] — Empty when cadence is `total`.
    - `net_earnings` number, required — TikTok's own bottom line for the window, from the daily series — the only source that includes GMV Max ad spend.
    - `cogs` number, required — Cost of goods on units sold. Negative, or 0.0 when unknown.
    - `sample_cost` number, required — Cost of samples sent. Negative, or 0.0 when unknown.
    - `contribution_profit` number, required — net_earnings + cogs + sample_cost. An UPPER BOUND whenever units_uncosted or sample_units_uncosted is non-zero — those units are counted as free, not guessed at.
    - `units` integer, required — Units on order lines in the window.
    - `units_costed` integer, required
    - `units_uncosted` integer, required — Units with no COGS on file. Fill these in via PUT /pnl/costs.
    - `units_on_refunded_lines` integer, required — Units sitting on a line that carries a refund. A PARTIAL refund flags the whole line, so this is an upper bound on units returned — it is reported, never netted out, because whether a refunded unit still costs you is your accounting policy, not ours.
    - `sample_units` integer, required — Sample sends in the window, from Reacher's own sample funnel. The Seller Center P&L export cannot supply this: free samples are not orders and never appear in it.
    - `sample_units_costed` integer, required
    - `sample_units_uncosted` integer, required
    - `period_start` string, required
  - `currency_mismatched_costs` integer, required — Cost rows skipped because they are priced in a different currency than the shop. Non-zero means some costs are silently absent.
  - `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` string, required — Latest export-run status: never_run | pending | parsed | empty | failed | no_permission.
    - `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

---

[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-service-production.skmtc.workers.dev/v1/apis/reacherapp/reacher-data-api/revisions/30786bf76d4b/schema)
