# Customer timeline

Everything a store did with one customer, newest first, plus what plugins recorded about them: orders, returns, the emails the store sent, abandoned checkouts, live chat conversations and support tickets, and the events your plugin and others posted. One list, one shape, one call.

## The scopes

- `customer-events:read` lists a customer's timeline. A session token carries it only for a dashboard user who may read customers, the same way `customer:read` works.
- `customer-events:create` records your plugin's own events. It is plugin-bound: no dashboard permission stands behind it, so every session token carries it once the store has granted it.

## Reading the timeline

```http
GET /v1/customers/{customerId}/events?limit=50&types=order,plugin HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer <access-token>
```

| Parameter | Purpose |
| --- | --- |
| `limit` | 1 to 100 events a page, 50 by default. |
| `types` | A comma list of the kinds wanted: `order`, `return`, `email`, `checkout`, `chat`, `ticket`, `plugin`. Every kind when left out. `plugin` covers what every plugin recorded. |
| `before` | Only events before this instant (ISO 8601). Pass the previous page's `meta.nextBefore` to read on. |

The customer must belong to the store (`404` otherwise). The answer, in the `data` field of the standard response envelope, is `{ items, meta: { limit, nextBefore } }`; `nextBefore` is `null` on the last page. Every event has the same shape:

```json
{
  "id": "0f3c8b2a-1d4e-4a6b-9c2f-7e5a1b2c3d4e",
  "type": "order.placed",
  "occurredAt": "2026-10-01T10:00:00.000Z",
  "title": "Order #1001",
  "summary": "paid, processing",
  "link": "/orders/0f3c8b2a-1d4e-4a6b-9c2f-7e5a1b2c3d4e/detail",
  "data": {
    "orderNumber": "1001",
    "total": 12.5,
    "currency": "GBP",
    "status": "processing",
    "paymentStatus": "paid"
  }
}
```

| Type | What it is | `data` |
| --- | --- | --- |
| `order.placed` | An order the customer placed | `orderNumber`, `total`, `currency`, `status`, `paymentStatus` |
| `return.requested` | A return they asked for | `returnNumber`, `status`, `orderId` |
| `email.sent` | An email the store sent them | `template`, `status`, `openedAt`, `clickedAt` |
| `checkout.abandoned` | A checkout they left | `total`, `currency` |
| `chat.started` | A live chat conversation they started | `status`, `lastMessageAt` |
| `ticket.opened` | A support ticket they opened | `number`, `status` |
| `plugin.<pluginId>.<type>` | What a plugin recorded, with `plugin: { pluginId }` beside it | Whatever that plugin attached |

`link` is a path in the store dashboard where there is one, `null` otherwise. `id` is the id of the order, return, email, checkout, conversation, ticket or event itself, so it is stable across pages.

## Recording your own events

```http
POST /v1/customers/{customerId}/events HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "type": "deal.won",
  "title": "Deal won",
  "summary": "Acme renewal, 12,000 a year",
  "route": "/deals/42",
  "occurredAt": "2026-10-06T09:00:00.000Z",
  "externalId": "deal-42-won",
  "data": { "dealId": "d-1" }
}
```

| Field | Purpose |
| --- | --- |
| `type` | Your name for the event: lower-case dotted segments such as `deal.won`, each starting with a letter and then letters, digits, `_` or `-`, up to 60 characters. It may not start with `plugin.`, which Orbit adds: listed as `plugin.<pluginId>.<type>`. Required. |
| `title` | One line, up to 255 characters. Required. |
| `summary` | The detail under it, up to 2000 characters. |
| `route` | Where inside your plugin page a click lands, as a path such as `/deals/42`. Listed as the event's `link`, `/plugins/<pluginId>?route=%2Fdeals%2F42` (the route URL-encoded). |
| `occurredAt` | When it happened (ISO 8601). Now when left out. |
| `externalId` | Your own id for the event, up to 100 characters, unique within your plugin for the store (not per customer). A second post with the same id records nothing and answers the first, even under another customer, so a retried webhook handler never records twice. |
| `data` | An object your plugin wants back with the event, up to 4000 characters as JSON. |

The answer is `{ created, event }`; `created` is `false` when `externalId` matched. Other plugins holding `customer-events:read` see your events too, so record what a person would want to see on the customer's page, not a log.

## Limits

- Reading counts as a read on this API (600 a minute per install); recording is held to 120 a minute.
- The timeline reads the store's live data each time and is not a search. Narrow with `types` when you only need some kinds, and page with `before`.
- When a store deletes a customer, their timeline answers `404` and takes no new events.
