{/* Metadata lives in ./layout.tsx */}

# API reference

The Pluto Profit API exposes the same numbers the dashboard shows over plain REST, plus write endpoints to push orders and costs from any platform. One version track: `v1`. {{ className: 'lead' }}

<Warning>
  The public API is not live yet. The surface below describes the planned v1
  wire contract and may change before launch. Key prefixes (`pk_plp_` /
  `sk_plp_`) are final.
</Warning>

---

## Base URL

<Row>
 <Col>

Every path is relative to the API host. All responses are JSON. Money is always a **decimal string**, never a JSON number, so nothing is lost to floating-point rounding.

 </Col>
 <Col sticky>

 <CodeGroup title="Host">

```txt {{ title: 'v1' }}
https://api.plutoprofit.com
```

 </CodeGroup>

 </Col>
</Row>

---

## Authentication

<Row>
 <Col>

Create an API key at **app.plutoprofit.com/developers**. The key is shown once.

| Kind | Prefix | Powers |
| --- | --- | --- |
| Publishable | `pk_plp_` + 64 hex | read-only (`read`) |
| Secret | `sk_plp_` + 64 hex | scopes: `read`, `orders:write`, `costs:write`, `products:write` |
| Pixel token | `pt_plp_` + 64 hex | `collect` only: append events to `/v1/collect`, safe to embed in a page |

Keys minted before 2026-07-15 may use the legacy `pk_` / `sk_` prefix; lookup is hash-based, so they keep working. Send the full key as a bearer token on every request.

A `pk_plp_` key on a write route returns **403** with code `forbidden_scope` (or 401 with an explicit secret-key message). Publishable keys never silently gain write powers.

 </Col>
 <Col sticky>

 <CodeGroup title="Request">

```sh {{ title: 'curl' }}
curl https://api.plutoprofit.com/v1/metrics \
  -H "Authorization: Bearer pk_plp_your_key"
```

 </CodeGroup>

 </Col>
</Row>

---

## Endpoint map

### Read (publishable or secret with `read`)

<Properties>
  <Property name="GET /v1/metrics">
    Profit metrics for a date range: revenue, costs, ad spend, net profit.
  </Property>
  <Property name="GET /v1/metrics/definitions">
    Metric catalog: what each metric means and how it is calculated.
  </Property>
  <Property name="GET /v1/connections">
    Which sources are connected and when they last synced.
  </Property>
  <Property name="GET /v1/health">Service status.</Property>
  <Property name="GET /v1/ingest/status">
    What the write API accepted, rejected, and superseded, plus cost coverage.
  </Property>
</Properties>

### Write (secret key + scope)

<Properties>
  <Property name="PUT /v1/source">
    Register platform label, shop currency, and IANA timezone before first push.
  </Property>
  <Property name="POST /v1/orders">
    Batch upsert orders with line items and embedded refunds (`orders:write`).
  </Property>
  <Property name="PUT /v1/products/costs">
    Time-versioned SKU cost catalog (`products:write`).
  </Property>
  <Property name="POST /v1/costs/spend">
    Per-day custom ad spend into `ad_insights_daily` (`costs:write`).
  </Property>
  <Property name="GET|POST|PATCH|DELETE /v1/costs/expenses">
    Recurring expenses over the `custom_costs` substrate (`costs:write`).
  </Property>
</Properties>

### Collect (pixel token)

<Properties>
  <Property name="POST /v1/collect">
    Browser events from the Pluto pixel, batched, always 204. Authenticated by
    a `pt_plp_` token that can do nothing else.
  </Property>
</Properties>

Full request and response schemas: [Metrics](/developers/api/v1/metrics), [Connections](/developers/api/v1/connections), [Orders](/developers/api/v1/orders), [Product costs](/developers/api/v1/products), [Costs](/developers/api/v1/costs), [Collect](/developers/api/v1/collect), [Status](/developers/api/v1/status), [Errors](/developers/api/v1/errors), [Rate limits](/developers/api/v1/rate-limits).

---

## Versioning

Path versioning: `/v1`. **Additive-only within v1**: new optional fields and new endpoints only. Renames, removals, or semantic changes require `/v2`.

Unknown request fields are **rejected** (`unknown_field`) rather than ignored, so a typo cannot silently drop money.

---

## Money contract

Every money field is a JSON **string** matching non-negative decimal with at most 6 places, e.g. `"149.50"`. A JSON number in a money position is rejected with `money_must_be_string`. Pluto never parses money through `f64`.
