# Deliverables · Customer API

> Customer API reference for deliverables: GET /deliverables, GET /deliverables/{deliverableId}, GET /deliverables/{deliverableId}/progress, GET /deliverables/{deliverableId}/certificate.

> Markdown version of https://kanorio.com/en/developers/customer-api/deliverables. 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)

### Deliverables the customer has access to

`GET /deliverables`

- Stability: beta

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Maximum number of items to return, 1-100. Defaults to 20. |
| `cursor` | query | string | no | Opaque cursor from the previous page's nextCursor. Omit for the first page. |
| `locale` | query | string | no | Locale code (any letter case) or URL prefix of a live locale. Omitted: the customer's own locale (Contact.locale, set by their last checkout, sign-in or form submission), falling back to the site's primary locale. Titles and descriptions without a translation fall back to the primary locale. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].resolvedLocale` | string | no | Multilingual sites only: the locale actually served (the requested locale when a translation exists, otherwise the primary locale). |
| `data[].id` | string | yes | — |
| `data[].title` | string | yes | — |
| `data[].description` | string \| null | yes | — |
| `data[].structure` | "COURSE" \| "DOWNLOAD" | yes | — |
| `data[].coverAssetId` | string \| null | yes | — |
| `data[].cover` | object \| null | no | Cover image of the deliverable (null when unset or the image is in the trash). |
| `data[].cover.url` | string | yes | — |
| `data[].cover.width` | integer \| null | yes | — |
| `data[].cover.height` | integer \| null | yes | — |
| `data[].cover.alt` | string \| null | yes | — |
| `data[].cover.placeholder` | string \| null | yes | 16px WebP data URL |
| `data[].grantedAt` | string | yes | — |
| `data[].expiresAt` | string \| null | yes | — |
| `data[].expired` | boolean | yes | — |
| `data[].availableFrom` | string \| null | yes | — |
| `data[].sources` | string[] | yes | — |
| `data[].unitCount` | integer | yes | — |
| `data[].progress` | object \| null | yes | — |
| `data[].progress.percent` | number | yes | — |
| `data[].progress.completed` | boolean | yes | Completion snapshot: set the first time the completion rule is met and never reverts, even if units are added later. |
| `data[].progress.completedAt` | string \| null | no | — |
| `data[].progress.watchedSeconds` | integer | no | Wall-clock seconds of media playback across the deliverable (only on the deliverables list). |
| `data[].progress.lastPlayedAt` | string \| null | no | Latest media playback (only on the deliverables list). |
| `hasMore` | boolean | yes | True when another page exists; pass nextCursor to fetch it. |
| `nextCursor` | string \| null | yes | Cursor for the next page, or null on the last page. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `INVALID_CURSOR` | 400 | — |
| `INVALID_LIMIT` | 400 | — |

```bash
curl -X GET "https://{site}/api/v1/customer/deliverables" \
  -H "Authorization: Bearer $KANORIO_CUSTOMER_TOKEN"
```

### Deliverable tree with per-unit access and progress

`GET /deliverables/{deliverableId}`

- Stability: beta

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `deliverableId` | path | string | yes | — |
| `locale` | query | string | no | Locale code (any letter case) or URL prefix of a live locale. Omitted: the customer's own locale (Contact.locale, set by their last checkout, sign-in or form submission), falling back to the site's primary locale. Titles and descriptions without a translation fall back to the primary locale. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deliverable` | object | yes | — |
| `deliverable.resolvedLocale` | string | no | Multilingual sites only: the locale actually served (the requested locale when a translation exists, otherwise the primary locale). |
| `deliverable.id` | string | yes | — |
| `deliverable.title` | string | yes | — |
| `deliverable.description` | string \| null | yes | — |
| `deliverable.structure` | "COURSE" \| "DOWNLOAD" | yes | — |
| `deliverable.coverAssetId` | string \| null | yes | — |
| `deliverable.cover` | object \| null | no | Cover image of the deliverable (null when unset or the image is in the trash). |
| `deliverable.cover.url` | string | yes | — |
| `deliverable.cover.width` | integer \| null | yes | — |
| `deliverable.cover.height` | integer \| null | yes | — |
| `deliverable.cover.alt` | string \| null | yes | — |
| `deliverable.cover.placeholder` | string \| null | yes | 16px WebP data URL |
| `deliverable.authors` | object[] | no | Authors/instructors in the owner's order (shared site author list). Older servers omit it. |
| `deliverable.authors[].id` | string | yes | — |
| `deliverable.authors[].name` | string | yes | — |
| `deliverable.authors[].title` | string \| null | yes | Job title or credential, e.g. Head Chef |
| `deliverable.authors[].avatar` | string \| null | yes | Displayable image URL |
| `deliverable.authors[].bio` | string \| null | yes | — |
| `deliverable.authors[].url` | string \| null | yes | — |
| `deliverable.settings` | object | yes | — |
| `deliverable.units` | object[] | yes | — |
| `deliverable.units[].id` | string | yes | — |
| `deliverable.units[].parentId` | string \| null | yes | — |
| `deliverable.units[].order` | integer | yes | — |
| `deliverable.units[].kind` | string | yes | — |
| `deliverable.units[].title` | string \| null | yes | — |
| `deliverable.units[].description` | string \| null | yes | — |
| `deliverable.units[].isPreview` | boolean | yes | — |
| `deliverable.units[].allowDownload` | boolean | yes | — |
| `deliverable.units[].countsTowardCompletion` | boolean | no | false = bonus unit: not counted in percent/completion and never locked by the require-in-order rule. |
| `deliverable.units[].isPrerequisite` | boolean | no | Units after this one stay locked until it is completed. |
| `deliverable.units[].hasFile` | boolean | yes | — |
| `deliverable.units[].embed` | object \| null | yes | — |
| `deliverable.units[].body` | any | yes | — |
| `deliverable.units[].duration` | number \| null | yes | — |
| `deliverable.units[].pageCount` | integer \| null | yes | — |
| `deliverable.units[].mimeType` | string \| null | yes | — |
| `deliverable.units[].access` | object | yes | — |
| `deliverable.units[].progress` | "not_started" \| "in-progress" \| "completed" | yes | — |
| `deliverable.units[].positionSeconds` | number | yes | Furthest position reached (seconds); drives progress |
| `deliverable.units[].lastPosition` | number | no | Where the learner last stopped (seconds); resume from here. Absent on older servers. |
| `deliverable.units[].coveredPercent` | integer \| null | no | Share of the media actually watched (union of played ranges), 0-100; null when there is no duration or nothing watched yet |
| `deliverable.units[].quiz` | object \| null | no | — |
| `deliverable.progress` | object \| null | yes | — |
| `deliverable.progress.percent` | number | yes | — |
| `deliverable.progress.completed` | boolean | yes | Completion snapshot: set the first time the completion rule is met and never reverts, even if units are added later. |
| `deliverable.progress.completedAt` | string \| null | no | — |
| `deliverable.progress.watchedSeconds` | integer | no | Wall-clock seconds of media playback across the deliverable (only on the deliverables list). |
| `deliverable.progress.lastPlayedAt` | string \| null | no | Latest media playback (only on the deliverables list). |
| `deliverable.certificate` | object \| null | no | — |
| `deliverable.certificate.enabled` | boolean | yes | — |
| `deliverable.certificate.serial` | string \| null | yes | — |
| `deliverable.certificate.issuedAt` | string \| null | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `DELIVERABLE_NOT_FOUND` | 404 | — |

```bash
curl -X GET "https://{site}/api/v1/customer/deliverables/{deliverableId}" \
  -H "Authorization: Bearer $KANORIO_CUSTOMER_TOKEN"
```

### Progress summary for a deliverable

`GET /deliverables/{deliverableId}/progress`

- Stability: beta

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `deliverableId` | path | string | yes | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `units` | object | yes | — |
| `positions` | object | yes | — |
| `percent` | number | yes | — |
| `completed` | boolean | yes | — |
| `completedAt` | string \| null | no | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `DELIVERABLE_NOT_FOUND` | 404 | — |

```bash
curl -X GET "https://{site}/api/v1/customer/deliverables/{deliverableId}/progress" \
  -H "Authorization: Bearer $KANORIO_CUSTOMER_TOKEN"
```

### Short-lived download URL for the completion certificate (issued once the deliverable is completed and certificates are enabled)

`GET /deliverables/{deliverableId}/certificate`

- Stability: beta

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `deliverableId` | path | string | yes | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `certificate` | object | yes | — |
| `certificate.serial` | string | yes | — |
| `certificate.issuedAt` | string | yes | — |
| `certificate.url` | string | yes | Signed PDF URL, valid for expiresIn seconds |
| `certificate.expiresIn` | integer | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `DELIVERABLE_NOT_FOUND` | 404 | — |
| `CERTIFICATE_DISABLED` | 404 | — |
| `SIGN_FAILED` | 503 | — |
| `FORBIDDEN` | 403 | — |

```bash
curl -X GET "https://{site}/api/v1/customer/deliverables/{deliverableId}/certificate" \
  -H "Authorization: Bearer $KANORIO_CUSTOMER_TOKEN"
```
