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

# Analytics API

> Read-only access to your store's order and claim analytics — raw tables and pre-computed metrics.

The Analytics API gives you programmatic, read-only access to your store's analytics: raw data tables (orders, claims, addresses, claim items, resolutions, and per-line resolution economics) and pre-computed metrics (attach rate, claim rate, and more). Use it to feed OrderProtection data into your own dashboards, BI tools, or reports.

## Authentication

Every request needs a Bearer token carrying the `analytics:read` scope. Two token types work:

* A **[Personal Access Token](/developer/personal-access-tokens)** (`op_pat_`) — for accessing your own account's stores.
* An **[OAuth access token](/developer/authentication)** (`op_at_`) — for an app a merchant installed and granted `analytics:read`.

```bash theme={null}
curl -X GET "https://api.production.orderprotection.com/v1/analytics/metrics/order_attach_rate/timeseries?granularity=month" \
  -H "Authorization: Bearer op_pat_8508b7432c45f79827460fea_ajhnyH..."
```

<Warning>
  A token without the `analytics:read` scope receives `403 Forbidden`. Requesting a store outside your token's authorized set receives `403` with code `STORE_OUT_OF_SCOPE`.
</Warning>

### Store scoping

Results are always limited to your account and the stores your token is authorized for. By default an endpoint returns data for **all** of the token's authorized stores. Pass `store_ids` to narrow to specific stores:

```
?store_ids=store_abc123&store_ids=store_def456
```

The `store_ids` you receive back in the response `meta` reflect the exact scope that was applied.

## Data freshness

Analytics tables are rebuilt on a batch schedule (roughly every 8 hours), so data is **not** real-time and can be **up to \~8 hours behind** your live store data. Every response includes a `data_freshness_at` timestamp in its `meta` block — the time the underlying table was last rebuilt by the batch job (not a per-row timestamp):

```json theme={null}
"meta": { "data_freshness_at": "2026-07-08T06:00:00.000Z" }
```

## Tables

Tables return raw, row-level records.

<Note>
  **No PII is exposed.** Each table has a fixed allowlist of safe columns; personally identifiable fields (customer name, email, etc.) are never included and cannot be requested. The `fields` parameter can only ever *narrow* to columns within that allowlist — asking for a field outside it is dropped, and a request for *only* disallowed fields returns `400 INVALID_FIELDS`.
</Note>

### List tables

```
GET /v1/analytics/tables
```

Returns the available tables, each with its selectable columns.

```json theme={null}
{
  "data": [
    { "key": "orders", "safeColumns": ["order_id", "store_id", "created_at", "..."], "sortColumns": ["created_at", "order_total"] },
    { "key": "claims", "safeColumns": ["claim_id", "order_id", "claim_state", "..."], "sortColumns": ["created_at", "updated_at"] }
  ],
  "meta": { "data_freshness_at": "2026-07-08T06:00:00.000Z" }
}
```

### Get rows

```
GET /v1/analytics/tables/{name}
```

| Parameter | Description |
| - | - |
| `store_ids` | Repeatable. Restrict to specific stores within your scope. |
| `from` | Start of the time window (ISO 8601). Defaults to 30 days ago. |
| `to` | End of the time window (ISO 8601). Defaults to now. |
| `fields` | Comma-separated subset of the table's columns. Defaults to all safe columns. |
| `limit` | Page size. Defaults to 1000, maximum 10000. |
| `cursor` | Opaque cursor from a previous response's `meta.next_cursor`. |

```json theme={null}
{
  "data": [
    { "order_id": "ord_...", "store_id": "store_abc123", "created_at": "2026-07-01T12:00:00.000Z", "order_total": 84.99, "currency": "USD" }
  ],
  "meta": {
    "data_freshness_at": "2026-07-08T06:00:00.000Z",
    "store_ids": ["store_abc123"],
    "next_cursor": "eyJ2Ijpbl...",
    "has_more": true
  }
}
```

To page through results, resend the request with `cursor` set to `meta.next_cursor` until `has_more` is `false`.

#### Available tables

| Table | Grain | Columns |
| - | - | - |
| `orders` | one row per order | `order_id`, `store_id`, `account_id`, `policy_id`, `created_at`, `order_total`, `currency`, `platform`, `source_order_number` |
| `claims` | one row per claim | `claim_id`, `order_id`, `store_id`, `account_id`, `claim_type`, `claim_state`, `claim_category`, `created_at`, `updated_at` |
| `claim_items` | one row per claim line item | `claim_item_id`, `claim_id`, `order_id`, `store_id`, `account_id`, `sku`, `item_name`, `quantity`, `is_additional_item`, `manually_added`, `claim_type`, `claim_subtype_name`, `details`, `resolution_requested`, `source_order_number`, `created_at`, `updated_at` |
| `resolutions` | one row per resolution | `resolution_id`, `claim_id`, `order_id`, `store_id`, `account_id`, `resolution_type`, `paid_value`, `paid_currency`, `paid_original_value`, `credit_store`, `include_shipping`, `shipping_amount`, `claim_state`, `claim_category`, `is_ota`, `sales_channel`, `source_order_number`, `source_order_id`, `app_version`, `store_type`, `created_at` |
| `resolutions_items` | one row per resolution line (per SKU) | `resolution_line_id`, `resolution_id`, `claim_item_id`, `claim_id`, `order_id`, `store_id`, `account_id`, `parent_account_id`, `source_order_id`, `source_variant_id`, `source_product_id`, `sku`, `item_name`, `product_image`, `claim_type`, `claim_subtype_name`, `claim_category`, `sales_channel`, `claim_filed_from`, `details`, `order_item_quantity`, `total_order_item_amount`, `submitted_quantity`, `submitted_resolution`, `total_item_submitted_resolution_amount`, `actual_resolution`, `actual_resolution_quantity`, `total_item_actual_resolution_amount`, `claim_added_item`, `resolution_qty_less_than_submitted_qty`, `submitted_qty_less_than_order_qty`, `reship_order_number`, `created_at`, `claim_date`, `resolution_date`, `order_date` |
| `addresses` | one row per order (deduped) | `order_id`, `claim_id`, `customer_id`, `store_id`, `account_id`, `city`, `state`, `state_abr`, `zip_code`, `country`, `country_abr`, `latitude`, `longitude`, `address_type`, `order_date`, `claim_date` |

<Note>
  **`addresses` is geo-only.** Customer identity fields — name, email, phone, and street lines (`address1`/`address2`) — are excluded and cannot be requested. Coordinates are **rounded to \~1.1 km** (2 decimal places), so `latitude`/`longitude` support regional mapping but never pinpoint an individual address. Its time column is `order_date`.
</Note>

<Note>
  **Per-item dollars live on `resolutions_items`, not `claim_items`.** A single claim item can be split across multiple resolutions with different types (e.g. partial refund + partial reship on the same SKU). That means one `claim_items` row can map to N `resolutions_items` rows — join them on `claim_item_id` to get every payout tied to its item:

  ```
  claim_items ⟶ resolutions_items  (via claim_item_id, one-to-many)
  ```

  `total_item_actual_resolution_amount` on `resolutions_items` is the paid amount for that specific line; `shipping_amount` on `resolutions` is a separate line and is **not** included in `paid_value`.
</Note>

## Metrics

Metrics are pre-defined aggregations computed at query time over the batch-refreshed tables. Query them as a time series or fetch the latest single value.

### List metrics

```
GET /v1/analytics/metrics
```

```json theme={null}
{
  "data": [
    { "key": "order_attach_rate", "description": "Share of orders that carry an Order Protection policy.", "granularitySupport": ["day", "week", "month", "quarter", "year"] }
  ],
  "meta": { "data_freshness_at": "2026-07-08T06:00:00.000Z" }
}
```

### Time series

```
GET /v1/analytics/metrics/{key}/timeseries
```

| Parameter | Description |
| - | - |
| `store_ids` | Repeatable. Restrict to specific stores within your scope. |
| `from` | Start of the window (ISO 8601). Defaults to 30 days ago. |
| `to` | End of the window (ISO 8601). Defaults to now. Windows are capped at 366 days. |
| `granularity` | Bucket size: `day`, `week`, `month`, `quarter`, or `year`. |
| `group_by` | Set to `store_id` to return one series per store instead of an aggregate. |

```json theme={null}
{
  "data": [
    { "bucket": "2026-05-01T00:00:00.000Z", "value": 0.3712 },
    { "bucket": "2026-06-01T00:00:00.000Z", "value": 0.3302 }
  ],
  "meta": {
    "data_freshness_at": "2026-07-08T06:00:00.000Z",
    "store_ids": ["store_abc123"],
    "granularity": "month"
  }
}
```

When `group_by=store_id`, each data point also includes a `store_id` field.

### Latest value

```
GET /v1/analytics/metrics/{key}
```

Returns the single most recent value for the metric.

```json theme={null}
{
  "data": { "key": "order_attach_rate", "value": 0.3302 },
  "meta": {
    "data_freshness_at": "2026-07-08T06:00:00.000Z",
    "store_ids": ["store_abc123"],
    "from": "2026-06-08T00:00:00.000Z",
    "to": "2026-07-08T00:00:00.000Z"
  }
}
```

#### Available metrics

| Metric | Description |
| - | - |
| `order_attach_rate` | Share of orders that carry an Order Protection policy. |
| `protected_order_count` | Count of protected orders (distinct policies). |
| `claim_count` | Count of claims filed. |
| `claim_rate` | Claims per protected order, bucketed by granularity. |

## Limits

| Limit | Value |
| - | - |
| Default time window | 30 days |
| Maximum metric window | 366 days |
| Default table page size | 1000 rows |
| Maximum table page size | 10000 rows |

<Tip>
  For pulls larger than these limits, page through tables with the `cursor` parameter, or narrow your `from`/`to` window and combine the results.
</Tip>


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