> ## Documentation Index
> Fetch the complete documentation index at: https://docs.higherfrequency.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner analytics API

> A read-only, server-to-server API that a partner's backend polls to see how its referral program is performing: clicks, conversions, reversals and...

A read-only, server-to-server API that a partner's backend polls to see how its referral program is performing: clicks, conversions, reversals and commission, for the stores it operates.

* **Server to server only.** Call it from your backend, never from a browser. The key must stay secret.
* **Informational, never a payout surface.** Credit your users only from the payout endpoint of your partner wire and its checkpoint protocol. These figures come from the same ledger, aggregated differently, without the anti-double-credit protocol.
* **Additive versioning.** Fields may be added; existing fields are never removed or repurposed. Ignore keys you do not recognise.

## Authentication

```http theme={null}
GET /partner/stores/{storeId}/analytics
Authorization: Bearer <partner-analytics-key>
```

* Paths are served at the root, **without** `/api`.
* Each key is issued per partner and **bound to an explicit list of store ids**. A valid key used on a store outside that list gets the same `404` as a store that does not exist.
* This is a read credential. It is never the secret you use to sign order events; the two are deliberately separate so a leaked read key cannot forge orders.
* Every response carries `Cache-Control: no-store`. Do not cache responses.

## Endpoints

| Route | Returns | Rate limit (per calling IP) |
| - | - | - |
| `GET /partner/stores/{storeId}/analytics` | Lifetime totals for the store | 120/min |
| `GET /partner/stores/{storeId}/campaigns` | Each campaign with lifetime counters and earnings | 120/min |
| `GET /partner/stores/{storeId}/campaigns/{campaignId}/publishers` | Each provisioned creator's counters on one campaign (paged) | 120/min |
| `GET /partner/stores/{storeId}/campaigns/{campaignId}/publishers/{externalUserId}/clicks` | One creator's clicks on one campaign, newest first (paged) | 120/min |
| `GET /partner/stores/{storeId}/analytics/series` | Per-day or per-hour buckets over a window | 30/min |

### Store totals

```json theme={null}
{
  "storeId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "scope": "lifetime",
  "publishers": { "provisioned": 42, "withConversions": 17 },
  "clicks": { "total": 1250, "firstAt": "2026-01-10T09:00:00.000Z", "lastAt": "2026-09-11T14:22:31.000Z" },
  "conversions": { "total": 94, "firstOccurredAt": "2026-01-11T13:45:00.000Z", "lastOccurredAt": "2026-09-11T14:20:00.000Z" },
  "reversals": { "affectedConversions": 6 },
  "earnings": { "currency": "USD", "held": "12.34", "releasedLifetime": "240.00", "reversedLifetime": "15.00" },
  "asOf": "2026-09-11T14:22:35.123Z"
}
```

* `clicks.total` counts genuine redirect clicks across all of the store's campaigns, including ended ones.
* `conversions.total` counts distinct referred orders that converted. **A later refund does not remove an order** from this count.
* `reversals.affectedConversions` counts orders currently refunded, partially refunded or charged back.
* `held` is a point-in-time escrow balance and can go up or down. `releasedLifetime` and `reversedLifetime` only ever increase.
* `asOf` is the single database instant every figure was true at.

### Campaigns

Returns every campaign of the store in every state (`DRAFT`, `ACTIVE`, `END`, `COMPLETE`), newest first, capped at 100.

```json theme={null}
{
  "storeId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "scope": "lifetime",
  "earnings": { "currency": "USD", "unallocated": { "held": "0", "releasedLifetime": "0", "reversedLifetime": "0" } },
  "campaigns": [
    {
      "campaignId": "campaign_opaque_identifier",
      "title": "Spring referral programme",
      "scope": { "type": "product", "externalProductId": "9876543210" },
      "state": "ACTIVE",
      "rewardMode": "LAST_CLICK_ONLY",
      "lifecycle": { "createdAt": "2026-01-10T09:00:00.000Z", "campaignEndTime": "2026-09-30T23:59:59.000Z", "campaignCompleteTime": null },
      "summary": { "clicks": 1250, "conversions": 94, "reversals": 6, "returningOrders": 11 },
      "earnings": { "held": "12.34", "releasedLifetime": "240.00", "reversedLifetime": "15.00" },
      "publishersEnrolled": 42
    }
  ],
  "truncated": false,
  "asOf": "2026-09-11T14:22:35.123Z"
}
```

* `campaignId` is opaque. It is not a UUID; never parse it. `title` is display-only and may be `null`.
* `scope.externalProductId` is present only on `product` campaigns; the key is absent on `store-wide` ones.
* `rewardMode` is `LAST_CLICK_ONLY` (the last-click creator receives the whole order commission) or `FIRST_LAST_80_20` (if another eligible creator first introduced the shopper, they receive 20% of it and the last-click creator the remaining 80%). It is campaign metadata only: no `earnings` figure changes with it.
* `returningOrders` counts orders tracked but **not paid** because the buyer had already bought in this campaign. It never overlaps `conversions`.
* **Reconciliation guarantee.** For each of `held`, `releasedLifetime` and `reversedLifetime`, the sum over `campaigns[].earnings` plus `earnings.unallocated` equals the store total from the same snapshot, even when `truncated` is `true`. `unallocated` holds money that cannot be placed on a returned row (normally zero).
* Ended campaigns keep updating: a late refund still moves their reversals.

### Campaign creators

Query: `limit` (default 50, 1 to 100) and `cursor`.

```json theme={null}
{
  "storeId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "campaignId": "campaign_opaque_identifier",
  "scope": "lifetime",
  "publishers": [
    { "externalUserId": "2d8bbf43-43d3-4a7f-a67d-eab9aad31e81", "analytics": { "clicks": 27, "conversions": 4, "reversals": 1, "returningOrders": 2 } }
  ],
  "nextCursor": "eyJ2IjoxLCJhZnRlciI6Ii4uLiJ9",
  "asOf": "2026-09-11T14:22:35.123Z"
}
```

Every creator provisioned on the store is listed, including those with no activity (all zeros), ordered by `externalUserId`. This route carries **no money**.

### Creator clicks

Query: `limit` (default 100, 1 to 500) and `cursor`.

```json theme={null}
{
  "storeId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "campaignId": "campaign_opaque_identifier",
  "externalUserId": "2d8bbf43-43d3-4a7f-a67d-eab9aad31e81",
  "clicks": [
    { "clickId": "clk_550e8400-e29b-41d4-a716-446655440000", "occurredAt": "2026-09-11T14:22:31.000Z", "outcome": "converted", "returningCustomer": false }
  ],
  "nextCursor": null,
  "asOf": "2026-09-11T14:22:35.123Z"
}
```

`clickId` is your reconciliation key. `occurredAt` is when the **click** happened. `outcome` describes the click's own order:

| `outcome` | Meaning |
| - | - |
| `converted` | An eligible conversion is recorded against the click. |
| `refunded` | That conversion was reversed: full refund, partial refund or chargeback. |
| `rejected` | An order exists but earned no commission (unattributable, returning buyer, self-referral). |
| `no-conversion` | No order yet, or the order has no final decision yet. |

`returningCustomer` is `true` when any order through this click was tracked but not paid because the buyer was returning. **Outcomes change over time** (`no-conversion` to `converted` to `refunded`). Re-read by `clickId` and replace the older value.

### Time series

Query parameters:

| Parameter | Required | Meaning |
| - | - | - |
| `from` | Yes | Inclusive start. ISO-8601 **with** `Z` or an offset; date-only is rejected. |
| `to` | No | Exclusive end, default now. Must be after `from`. |
| `granularity` | Yes | `day` or `hour`. No default. |
| `timezone` | No | A named IANA zone such as `Europe/London`. Default `UTC`. Offsets like `+01:00` are rejected. |
| `campaignId` | No | Narrow to one campaign. An unknown id returns empty buckets, not `404`. |

Each bucket reports `clicks`, `conversions`, `commissionAccrued`, `released` and `reversed`, plus a `totals` object that always equals the sum of the buckets. `startsAt` is an absolute UTC instant; bucket boundaries follow the chosen zone, so a daylight-saving day has 23 or 25 hourly buckets. Empty buckets are included. At most 1,000 buckets per request.

The series reports **flows** only. There is no `held` (a balance has no meaning per bucket) and no reversal count. Conversions land on your event's occurrence time if you send one, otherwise on ingestion time, which can lag.

## Pagination

The creator and click routes use keyset cursors:

1. Call without `cursor`.
2. While `nextCursor` is not `null`, call again with `cursor=<nextCursor>` **exactly as received**, not re-encoded.
3. Stop when `nextCursor` is `null`.

There is no total count. `asOf` is per page, not a frozen snapshot of the walk. New clicks arrive at the **front** of the clicks feed, so restart from page one for a fresh reconciliation. Cursors are traversal tokens, not bookmarks; do not store or construct them.

## Errors

| Status | When |
| - | - |
| `400` | Bad `storeId` (must be a UUID v4), bad `limit`, unknown query parameter, invalid cursor, invalid `externalUserId` (1 to 64 of `A-Z a-z 0-9 . _ ~ -`), or invalid series window, zone or granularity. |
| `401` | Missing or invalid key. |
| `404` | Unknown, disconnected or unbound store; foreign or unknown campaign; unprovisioned user. All deliberately identical. |
| `429` | Rate limit exceeded. Back off. |
| `503` + `Retry-After` | A data-integrity guard refused to serve money figures (`code` is one of `ANALYTICS_INTEGRITY_NEGATIVE_ROWS`, `ANALYTICS_INTEGRITY_CURRENCY`, `ANALYTICS_INTEGRITY_UNEXPECTED_FEE`). Retry after the hint. |

## Gotchas

* **Money is a decimal string.** Parse with a decimal type, not a float.
* **Counts never shrink; balances can.** Use `releasedLifetime` deltas, not `held`, to reason about what was paid.
* **Late refunds.** A refund arriving after a commission was already released writes no reversal entry today, so `reversedLifetime` can understate total reversals.
* **Two requests are two snapshots.** The store totals and the campaigns list agree when read at the same instant; separate calls can differ by activity in between.
* **This is referred activity, not your order book.** Orders that were never referred are not counted.
* **Personal data.** `externalUserId` is your own pseudonymous id. It is returned only to the partner that supplied it. Use it only for referral attribution and reconciliation, and do not cache it beyond that. Requests are access-logged with credentials redacted.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.