# Progress · Customer API

> Customer API reference for progress: POST /progress.

> Markdown version of https://kanorio.com/en/developers/customer-api/progress. Append `.md` to any developer docs URL for this format. Index: https://kanorio.com/llms.txt · MCP: https://app.kanorio.com/api/mcp

Base URL: `https://{site}/api/v1/customer` · [Errors](https://kanorio.com/en/developers/basics/errors) · [Pagination](https://kanorio.com/en/developers/basics/pagination)

### Report playback/reading progress (server decides completion)

`POST /progress`

- Stability: beta

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `unitId` | string | yes | — |
| `reports` | object[] | yes | — |
| `reports[].positionSeconds` | number | no | Furthest position reached (seconds). Clients that only send this still resume and complete (by position ratio). |
| `reports[].durationSeconds` | number | no | — |
| `reports[].completed` | boolean | no | Manual 'mark complete' for non-media units; ignored for quiz units. |
| `reports[].opened` | boolean | no | Non-media unit opened: records the first open and sets the unit to in-progress; does not complete it. |
| `reports[].sessionId` | string | no | Browser-generated id of this viewing session; send the same value on every report of the session. Required for playback records (segments, play count, watch time). |
| `reports[].segments` | number[][] | no | Ranges played since the previous report as [from, to] or [from, to, speed] in media seconds. Completion uses the union of all ranges, so skipping ahead does not count. |
| `reports[].startupMs` | integer | no | — |
| `reports[].stallCount` | integer | no | — |
| `reports[].stallMs` | integer | no | — |
| `reports[].rendition` | integer | no | Current rendition height (px). |
| `reports[].seekTargets` | number[] | no | Seek destinations (seconds) since the previous report. |
| `reports[].device` | "desktop" \| "mobile" \| "tablet" | no | — |
| `lite` | boolean | no | Heartbeat: when this report does not complete the unit the server skips deliverable-level completion and returns deliverable: null. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `unit` | "not_started" \| "in-progress" \| "completed" | yes | — |
| `unitCoveredPercent` | integer \| null | no | Media units: share of the file watched after this report (union of watched ranges / duration, 0-100); null without a duration or range data. |
| `deliverable` | object \| null | yes | null on lite heartbeats that did not complete the unit |
| `deliverable.id` | string | yes | — |
| `deliverable.percent` | number | yes | — |
| `deliverable.completed` | boolean | yes | — |
| `deliverable.completedAt` | string \| null | no | — |
| `deliverableJustCompleted` | boolean | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `UNIT_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | — |
| `FORBIDDEN` | 403 | — |

```bash
curl -X POST "https://{site}/api/v1/customer/progress" \
  -H "Authorization: Bearer $KANORIO_CUSTOMER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```
