# Deliverables · Management API

> Management API reference for deliverables: GET /sites/{id}/deliverables, POST /sites/{id}/deliverables, GET /sites/{id}/deliverables/{deliverableId}, PATCH /sites/{id}/deliverables/{deliverableId}, ….

> Markdown version of https://kanorio.com/en/developers/management-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://app.kanorio.com/api/v1` · [Errors](https://kanorio.com/en/developers/basics/errors) · [Pagination](https://kanorio.com/en/developers/basics/pagination)

### List deliverables

`GET /sites/{id}/deliverables`

- Stability: beta
- Permission: `store:view`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_list`

List deliverables (courses, e-books, audio series, template packs) with unit counts and linked products. A deliverable is what a buyer receives; products only sell it. Errors: FORBIDDEN, INVALID_CURSOR.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `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. |
| `status` | query | "ALL" \| "ACTIVE" \| "ARCHIVED" | no | — |
| `search` | query | string | no | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].id` | string | yes | — |
| `data[].title` | string | yes | — |
| `data[].description` | string \| null | yes | — |
| `data[].coverAssetId` | string \| null | yes | — |
| `data[].structure` | "COURSE" \| "DOWNLOAD" | yes | COURSE has chapters and tracks progress; DOWNLOAD is a flat file list (no progress, new units allow download by default). Access rules are identical. |
| `data[].status` | "ACTIVE" \| "ARCHIVED" | yes | — |
| `data[].grants` | string[] | yes | Commercial feature keys (e.g. ["premium"]) granted when a buyer purchases or is granted this deliverable; matched by ContentCollection.accessPolicy and entitlements.check. |
| `data[].authorIds` | string[] | yes | Authors/instructors shown on the sales page and the learner page, in order. Ids come from the site's shared author list (also used by posts; managed in the console under Posts › Settings). |
| `data[].unitCount` | integer | yes | Leaf units only; chapters are not counted. |
| `data[].productIds` | string[] | yes | Products that grant this deliverable on purchase (see products.update deliverables). |
| `data[].createdAt` | string | yes | — |
| `data[].updatedAt` | string | yes | — |
| `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 |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `INVALID_CURSOR` | 400 | — |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/deliverables" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "data": [
    {
      "id": "dlv_example01",
      "title": "Sourdough Fundamentals",
      "description": "Six-week video course with downloadable recipe cards.",
      "coverAssetId": "asset_example01",
      "structure": "COURSE",
      "status": "ACTIVE",
      "grants": [
        "premium"
      ],
      "authorIds": [
        "author_headbaker01"
      ],
      "unitCount": 3,
      "productIds": [
        "prod_example01"
      ],
      "createdAt": "2026-10-01T02:30:00.000Z",
      "updatedAt": "2026-10-06T08:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Create a deliverable

`POST /sites/{id}/deliverables`

- Stability: beta
- Permission: `commerce:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_create`

Create a deliverable. Add chapters and units with deliverable_units.replace_tree, then link it to a product with products.update({ deliverables }) or grant it directly with entitlements.grant. Use structure DOWNLOAD for a plain file bundle (e-book, template pack) and COURSE when learners should see chapters and progress. The structure cannot be changed later; to switch, copy the deliverable with deliverables.duplicate. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND, VALIDATION (details.issues), ASSET_NOT_FOUND (details.assetIds: not in the media library, in the trash or an older version), ASSET_NOT_ALLOWED (coverAssetId, certificate.signatureAssetId or certificate.backgroundAssetId is not a public image), AUTHOR_NOT_FOUND (details.authorIds: not in the site's author list).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes | — |
| `description` | string \| null | no | — |
| `coverAssetId` | string \| null | no | — |
| `structure` | "COURSE" \| "DOWNLOAD" | no | COURSE (default) for chapters/progress; DOWNLOAD for a flat file list. Fixed after creation. |
| `grants` | string[] | no | Optional feature keys (e.g. ["premium"]) unlocked by this deliverable for collection accessPolicy and entitlements.check. |
| `authorIds` | string[] | no | Authors/instructors (ids from the site's shared author list, replaced as a whole, order kept). Unknown ids fail with AUTHOR_NOT_FOUND. |
| `settings` | object | no | — |
| `settings.completionRule` | object | no | — |
| `settings.completionRule.type` | "all_units" | yes | — |
| `settings.certificate` | object | no | When enabled, a certificate (serial + issue date) is issued the first time a learner completes the deliverable; learners fetch it with Customer API GET /deliverables/:id/certificate. The PDF is generated on download from the current settings, course title, learner name and site branding (logo, brand colour), so edits apply to every certificate; only the serial and issue date are fixed. Send every certificate field when updating it. Use deliverables.certificate_preview to see it. |
| `settings.certificate.enabled` | boolean | yes | — |
| `settings.certificate.title` | string \| null | yes | Certificate heading; null = the site-language default ("Certificate of Completion"). |
| `settings.certificate.signatureName` | string \| null | yes | Signer's name or organisation; null = no signature block. |
| `settings.certificate.signatureTitle` | string \| null | yes | Signer's title printed under the name, e.g. "Lead instructor". |
| `settings.certificate.signatureAssetId` | string \| null | yes | Signature image: a public image asset id (same rule as coverAssetId); printed above the signature line. |
| `settings.certificate.showLogo` | boolean | yes | Print the site's brand logo (site settings) at the top; icon logos are skipped. |
| `settings.certificate.note` | string \| null | yes | One line printed under the course title, e.g. "12 hours of instruction". |
| `settings.certificate.backgroundAssetId` | string \| null | yes | Background image: a public image asset id (same rule as coverAssetId), centred and cropped to fill an A4 landscape page (recommended 3508 × 2480 px). When set, the default border is not drawn; text positions stay fixed, so keep the centre of the design clear. |
| `settings.certificate.backgroundOverlay` | boolean | yes | Lay a semi-transparent white layer over the background image so text stays readable on busy designs; no effect without a background image. |
| `settings.comments` | object | no | — |
| `settings.comments.enabled` | boolean | yes | — |
| `settings.comments.threaded` | boolean | yes | — |
| `settings.comments.requireReview` | boolean | yes | — |
| `settings.comments.reactions` | boolean | yes | — |
| `settings.requireSequential` | boolean | no | Learners must complete each unit before opening the next (free-preview units are exempt). Locked units come back with access.reason prerequisite. |
| `settings.mediaCompletionPercent` | integer | no | Share of a video/audio unit a learner must actually watch for it to count as completed (50-100, step 5, default 90). Measured from the segments played, so skipping to the end does not count; 100 keeps a 2-second tolerance at the end. Custom players that only report positionSeconds are measured by the furthest position. Raising it never un-completes learners. |
| `fieldData` | object \| null | no | — |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `title` | string | yes | — |
| `description` | string \| null | yes | — |
| `coverAssetId` | string \| null | yes | — |
| `structure` | "COURSE" \| "DOWNLOAD" | yes | COURSE has chapters and tracks progress; DOWNLOAD is a flat file list (no progress, new units allow download by default). Access rules are identical. |
| `status` | "ACTIVE" \| "ARCHIVED" | yes | — |
| `grants` | string[] | yes | Commercial feature keys (e.g. ["premium"]) granted when a buyer purchases or is granted this deliverable; matched by ContentCollection.accessPolicy and entitlements.check. |
| `authorIds` | string[] | yes | Authors/instructors shown on the sales page and the learner page, in order. Ids come from the site's shared author list (also used by posts; managed in the console under Posts › Settings). |
| `unitCount` | integer | yes | Leaf units only; chapters are not counted. |
| `productIds` | string[] | yes | Products that grant this deliverable on purchase (see products.update deliverables). |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `settings` | object | yes | — |
| `settings.completionRule` | object | yes | — |
| `settings.completionRule.type` | "all_units" | yes | — |
| `settings.certificate` | object | yes | When enabled, a certificate (serial + issue date) is issued the first time a learner completes the deliverable; learners fetch it with Customer API GET /deliverables/:id/certificate. The PDF is generated on download from the current settings, course title, learner name and site branding (logo, brand colour), so edits apply to every certificate; only the serial and issue date are fixed. Send every certificate field when updating it. Use deliverables.certificate_preview to see it. |
| `settings.certificate.enabled` | boolean | yes | — |
| `settings.certificate.title` | string \| null | yes | Certificate heading; null = the site-language default ("Certificate of Completion"). |
| `settings.certificate.signatureName` | string \| null | yes | Signer's name or organisation; null = no signature block. |
| `settings.certificate.signatureTitle` | string \| null | yes | Signer's title printed under the name, e.g. "Lead instructor". |
| `settings.certificate.signatureAssetId` | string \| null | yes | Signature image: a public image asset id (same rule as coverAssetId); printed above the signature line. |
| `settings.certificate.showLogo` | boolean | yes | Print the site's brand logo (site settings) at the top; icon logos are skipped. |
| `settings.certificate.note` | string \| null | yes | One line printed under the course title, e.g. "12 hours of instruction". |
| `settings.certificate.backgroundAssetId` | string \| null | yes | Background image: a public image asset id (same rule as coverAssetId), centred and cropped to fill an A4 landscape page (recommended 3508 × 2480 px). When set, the default border is not drawn; text positions stay fixed, so keep the centre of the design clear. |
| `settings.certificate.backgroundOverlay` | boolean | yes | Lay a semi-transparent white layer over the background image so text stays readable on busy designs; no effect without a background image. |
| `settings.comments` | object | yes | — |
| `settings.comments.enabled` | boolean | yes | — |
| `settings.comments.threaded` | boolean | yes | — |
| `settings.comments.requireReview` | boolean | yes | — |
| `settings.comments.reactions` | boolean | yes | — |
| `settings.requireSequential` | boolean | yes | Learners must complete each unit before opening the next (free-preview units are exempt). Locked units come back with access.reason prerequisite. |
| `settings.mediaCompletionPercent` | integer | yes | Share of a video/audio unit a learner must actually watch for it to count as completed (50-100, step 5, default 90). Measured from the segments played, so skipping to the end does not count; 100 keeps a 2-second tolerance at the end. Custom players that only report positionSeconds are measured by the furthest position. Raising it never un-completes learners. |
| `fieldData` | object \| null | yes | — |
| `units` | object[] | yes | Flat list ordered by chapter then unit; use parentId to rebuild the tree. |
| `units[].id` | string | yes | — |
| `units[].parentId` | string \| null | yes | Chapter id for units inside a chapter; null for top-level chapters or flat units. |
| `units[].order` | integer | yes | — |
| `units[].kind` | string | yes | Open string. Known values: video, audio, file, text, quiz; "chapter" for grouping rows. Unknown kinds render as a download card. |
| `units[].title` | string \| null | yes | — |
| `units[].description` | string \| null | yes | — |
| `units[].primaryAssetId` | string \| null | yes | Media-library asset (private products bucket). Upload with media.upload first. |
| `units[].attachmentAssetIds` | string[] | yes | — |
| `units[].fileUrl` | string \| null | yes | Only for Kanorio template sample media (not a media-library asset). Real files are referenced by primaryAssetId. |
| `units[].embed` | object \| null | yes | External video embed (youtube/vimeo/loom). Not protected by Kanorio; use for FREE sites that cannot host video. |
| `units[].embed.provider` | string | yes | — |
| `units[].embed.id` | string | yes | — |
| `units[].body` | any | yes | text units: { markdown }. quiz units: { questions: [{ id, type: "single" \| "multiple" \| "true_false", prompt, options: [{ id, text, correct: boolean }], explanation? }], settings: { passingPercent (0-100, default 70), maxAttempts (1-99 or null = unlimited), showAnswers: "never" \| "after_pass" \| "always" } }; validated on write (UNIT_TREE_INVALID quiz_body_invalid). Learners never see correct/explanation; the server grades Customer API POST /units/:id/quiz. |
| `units[].isPreview` | boolean | yes | Free preview: anonymous visitors can open it. |
| `units[].visibility` | "VISIBLE" \| "HIDDEN" | yes | — |
| `units[].releaseAt` | string \| null | yes | Absolute unlock time. |
| `units[].releaseAfterDays` | integer \| null | yes | Unlock N days after the learner's entitlement started. |
| `units[].allowDownload` | boolean | yes | — |
| `units[].countsTowardCompletion` | boolean | yes | false = bonus unit: not counted in percent or completion, never locked by requireSequential and never a gate for later units. |
| `units[].isPrerequisite` | boolean | yes | Units after this one stay locked until the learner completes it, even when requireSequential is off. |
| `products` | object[] | yes | Products (variants) that grant this deliverable; tierId null = every variant. Access duration is pricingTiers[].accessDays on the product. |
| `products[].productId` | string | yes | — |
| `products[].tierId` | string \| null | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `DELIVERABLE_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | details.issues |
| `ASSET_NOT_FOUND` | 404 | details.assetIds: not in the media library, in the trash or an older version |
| `ASSET_NOT_ALLOWED` | 422 | coverAssetId, certificate.signatureAssetId or certificate.backgroundAssetId is not a public image |
| `AUTHOR_NOT_FOUND` | 404 | details.authorIds: not in the site's author list |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/deliverables" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Sourdough Fundamentals","description":"Six-week video course with downloadable recipe cards.","coverAssetId":"asset_example01","structure":"COURSE"}'
```

```json
{
  "id": "dlv_example01",
  "title": "Sourdough Fundamentals",
  "description": "Six-week video course with downloadable recipe cards.",
  "coverAssetId": "asset_example01",
  "structure": "COURSE",
  "status": "ACTIVE",
  "grants": [
    "premium"
  ],
  "authorIds": [
    "author_headbaker01"
  ],
  "unitCount": 0,
  "productIds": [],
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "settings": {
    "completionRule": {
      "type": "all_units"
    },
    "certificate": {
      "enabled": false,
      "title": null,
      "signatureName": null,
      "signatureTitle": null,
      "signatureAssetId": null,
      "showLogo": true,
      "note": null,
      "backgroundAssetId": null,
      "backgroundOverlay": false
    },
    "comments": {
      "enabled": false,
      "threaded": true,
      "requireReview": true,
      "reactions": false
    },
    "requireSequential": false,
    "mediaCompletionPercent": 90
  },
  "fieldData": null,
  "units": [],
  "products": []
}
```

### Retrieve a deliverable

`GET /sites/{id}/deliverables/{deliverableId}`

- Stability: beta
- Permission: `store:view`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_get`

Get one deliverable with settings and the full chapter/unit tree (no signed file URLs). Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `deliverableId` | path | string | yes | Deliverable id. Get it from deliverables.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `title` | string | yes | — |
| `description` | string \| null | yes | — |
| `coverAssetId` | string \| null | yes | — |
| `structure` | "COURSE" \| "DOWNLOAD" | yes | COURSE has chapters and tracks progress; DOWNLOAD is a flat file list (no progress, new units allow download by default). Access rules are identical. |
| `status` | "ACTIVE" \| "ARCHIVED" | yes | — |
| `grants` | string[] | yes | Commercial feature keys (e.g. ["premium"]) granted when a buyer purchases or is granted this deliverable; matched by ContentCollection.accessPolicy and entitlements.check. |
| `authorIds` | string[] | yes | Authors/instructors shown on the sales page and the learner page, in order. Ids come from the site's shared author list (also used by posts; managed in the console under Posts › Settings). |
| `unitCount` | integer | yes | Leaf units only; chapters are not counted. |
| `productIds` | string[] | yes | Products that grant this deliverable on purchase (see products.update deliverables). |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `settings` | object | yes | — |
| `settings.completionRule` | object | yes | — |
| `settings.completionRule.type` | "all_units" | yes | — |
| `settings.certificate` | object | yes | When enabled, a certificate (serial + issue date) is issued the first time a learner completes the deliverable; learners fetch it with Customer API GET /deliverables/:id/certificate. The PDF is generated on download from the current settings, course title, learner name and site branding (logo, brand colour), so edits apply to every certificate; only the serial and issue date are fixed. Send every certificate field when updating it. Use deliverables.certificate_preview to see it. |
| `settings.certificate.enabled` | boolean | yes | — |
| `settings.certificate.title` | string \| null | yes | Certificate heading; null = the site-language default ("Certificate of Completion"). |
| `settings.certificate.signatureName` | string \| null | yes | Signer's name or organisation; null = no signature block. |
| `settings.certificate.signatureTitle` | string \| null | yes | Signer's title printed under the name, e.g. "Lead instructor". |
| `settings.certificate.signatureAssetId` | string \| null | yes | Signature image: a public image asset id (same rule as coverAssetId); printed above the signature line. |
| `settings.certificate.showLogo` | boolean | yes | Print the site's brand logo (site settings) at the top; icon logos are skipped. |
| `settings.certificate.note` | string \| null | yes | One line printed under the course title, e.g. "12 hours of instruction". |
| `settings.certificate.backgroundAssetId` | string \| null | yes | Background image: a public image asset id (same rule as coverAssetId), centred and cropped to fill an A4 landscape page (recommended 3508 × 2480 px). When set, the default border is not drawn; text positions stay fixed, so keep the centre of the design clear. |
| `settings.certificate.backgroundOverlay` | boolean | yes | Lay a semi-transparent white layer over the background image so text stays readable on busy designs; no effect without a background image. |
| `settings.comments` | object | yes | — |
| `settings.comments.enabled` | boolean | yes | — |
| `settings.comments.threaded` | boolean | yes | — |
| `settings.comments.requireReview` | boolean | yes | — |
| `settings.comments.reactions` | boolean | yes | — |
| `settings.requireSequential` | boolean | yes | Learners must complete each unit before opening the next (free-preview units are exempt). Locked units come back with access.reason prerequisite. |
| `settings.mediaCompletionPercent` | integer | yes | Share of a video/audio unit a learner must actually watch for it to count as completed (50-100, step 5, default 90). Measured from the segments played, so skipping to the end does not count; 100 keeps a 2-second tolerance at the end. Custom players that only report positionSeconds are measured by the furthest position. Raising it never un-completes learners. |
| `fieldData` | object \| null | yes | — |
| `units` | object[] | yes | Flat list ordered by chapter then unit; use parentId to rebuild the tree. |
| `units[].id` | string | yes | — |
| `units[].parentId` | string \| null | yes | Chapter id for units inside a chapter; null for top-level chapters or flat units. |
| `units[].order` | integer | yes | — |
| `units[].kind` | string | yes | Open string. Known values: video, audio, file, text, quiz; "chapter" for grouping rows. Unknown kinds render as a download card. |
| `units[].title` | string \| null | yes | — |
| `units[].description` | string \| null | yes | — |
| `units[].primaryAssetId` | string \| null | yes | Media-library asset (private products bucket). Upload with media.upload first. |
| `units[].attachmentAssetIds` | string[] | yes | — |
| `units[].fileUrl` | string \| null | yes | Only for Kanorio template sample media (not a media-library asset). Real files are referenced by primaryAssetId. |
| `units[].embed` | object \| null | yes | External video embed (youtube/vimeo/loom). Not protected by Kanorio; use for FREE sites that cannot host video. |
| `units[].embed.provider` | string | yes | — |
| `units[].embed.id` | string | yes | — |
| `units[].body` | any | yes | text units: { markdown }. quiz units: { questions: [{ id, type: "single" \| "multiple" \| "true_false", prompt, options: [{ id, text, correct: boolean }], explanation? }], settings: { passingPercent (0-100, default 70), maxAttempts (1-99 or null = unlimited), showAnswers: "never" \| "after_pass" \| "always" } }; validated on write (UNIT_TREE_INVALID quiz_body_invalid). Learners never see correct/explanation; the server grades Customer API POST /units/:id/quiz. |
| `units[].isPreview` | boolean | yes | Free preview: anonymous visitors can open it. |
| `units[].visibility` | "VISIBLE" \| "HIDDEN" | yes | — |
| `units[].releaseAt` | string \| null | yes | Absolute unlock time. |
| `units[].releaseAfterDays` | integer \| null | yes | Unlock N days after the learner's entitlement started. |
| `units[].allowDownload` | boolean | yes | — |
| `units[].countsTowardCompletion` | boolean | yes | false = bonus unit: not counted in percent or completion, never locked by requireSequential and never a gate for later units. |
| `units[].isPrerequisite` | boolean | yes | Units after this one stay locked until the learner completes it, even when requireSequential is off. |
| `products` | object[] | yes | Products (variants) that grant this deliverable; tierId null = every variant. Access duration is pricingTiers[].accessDays on the product. |
| `products[].productId` | string | yes | — |
| `products[].tierId` | string \| null | yes | — |

**Errors**

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

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/deliverables/{deliverableId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "id": "dlv_example01",
  "title": "Sourdough Fundamentals",
  "description": "Six-week video course with downloadable recipe cards.",
  "coverAssetId": "asset_example01",
  "structure": "COURSE",
  "status": "ACTIVE",
  "grants": [
    "premium"
  ],
  "authorIds": [
    "author_headbaker01"
  ],
  "unitCount": 3,
  "productIds": [
    "prod_example01"
  ],
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "settings": {
    "completionRule": {
      "type": "all_units"
    },
    "certificate": {
      "enabled": false,
      "title": null,
      "signatureName": null,
      "signatureTitle": null,
      "signatureAssetId": null,
      "showLogo": true,
      "note": null,
      "backgroundAssetId": null,
      "backgroundOverlay": false
    },
    "comments": {
      "enabled": false,
      "threaded": true,
      "requireReview": true,
      "reactions": false
    },
    "requireSequential": false,
    "mediaCompletionPercent": 90
  },
  "fieldData": null,
  "units": [
    {
      "id": "unit_chapter01",
      "parentId": null,
      "order": 0,
      "kind": "chapter",
      "title": "Week 1 · Getting started",
      "description": null,
      "primaryAssetId": null,
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": null,
      "isPreview": false,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": null,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    },
    {
      "id": "unit_video01",
      "parentId": "unit_chapter01",
      "order": 0,
      "kind": "video",
      "title": "Welcome and course overview",
      "description": "What you will build in six weeks.",
      "primaryAssetId": "asset_example01",
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": null,
      "isPreview": true,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": null,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    },
    {
      "id": "unit_text01",
      "parentId": "unit_chapter01",
      "order": 1,
      "kind": "text",
      "title": "Set up your workspace",
      "description": null,
      "primaryAssetId": null,
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": {
        "markdown": "## Install\n\n1. Download the starter kit…"
      },
      "isPreview": false,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": 2,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    }
  ],
  "products": [
    {
      "productId": "prod_example01",
      "tierId": null
    }
  ]
}
```

### Update a deliverable

`PATCH /sites/{id}/deliverables/{deliverableId}`

- Stability: beta
- Permission: `commerce:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_update`

Partially update a deliverable (title, description, cover, settings such as completionRule/certificate/comments). Units are managed with deliverable_units.*. The structure is fixed after creation: sending a different structure fails with STRUCTURE_LOCKED; copy it with deliverables.duplicate instead. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND, VALIDATION (details.issues), ASSET_NOT_FOUND (details.assetIds: not in the media library, in the trash or an older version), ASSET_NOT_ALLOWED (coverAssetId, certificate.signatureAssetId or certificate.backgroundAssetId is not a public image), AUTHOR_NOT_FOUND (details.authorIds: not in the site's author list).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `deliverableId` | path | string | yes | Deliverable id. Get it from deliverables.list. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | no | — |
| `description` | string \| null | no | — |
| `coverAssetId` | string \| null | no | — |
| `structure` | "COURSE" \| "DOWNLOAD" | no | Must equal the current structure (fixed after creation); omit it. |
| `grants` | string[] | no | Optional feature keys (e.g. ["premium"]) unlocked by this deliverable for collection accessPolicy and entitlements.check. |
| `authorIds` | string[] | no | Authors/instructors (ids from the site's shared author list, replaced as a whole, order kept). Unknown ids fail with AUTHOR_NOT_FOUND. |
| `settings` | object | no | — |
| `settings.completionRule` | object | no | — |
| `settings.completionRule.type` | "all_units" | yes | — |
| `settings.certificate` | object | no | When enabled, a certificate (serial + issue date) is issued the first time a learner completes the deliverable; learners fetch it with Customer API GET /deliverables/:id/certificate. The PDF is generated on download from the current settings, course title, learner name and site branding (logo, brand colour), so edits apply to every certificate; only the serial and issue date are fixed. Send every certificate field when updating it. Use deliverables.certificate_preview to see it. |
| `settings.certificate.enabled` | boolean | yes | — |
| `settings.certificate.title` | string \| null | yes | Certificate heading; null = the site-language default ("Certificate of Completion"). |
| `settings.certificate.signatureName` | string \| null | yes | Signer's name or organisation; null = no signature block. |
| `settings.certificate.signatureTitle` | string \| null | yes | Signer's title printed under the name, e.g. "Lead instructor". |
| `settings.certificate.signatureAssetId` | string \| null | yes | Signature image: a public image asset id (same rule as coverAssetId); printed above the signature line. |
| `settings.certificate.showLogo` | boolean | yes | Print the site's brand logo (site settings) at the top; icon logos are skipped. |
| `settings.certificate.note` | string \| null | yes | One line printed under the course title, e.g. "12 hours of instruction". |
| `settings.certificate.backgroundAssetId` | string \| null | yes | Background image: a public image asset id (same rule as coverAssetId), centred and cropped to fill an A4 landscape page (recommended 3508 × 2480 px). When set, the default border is not drawn; text positions stay fixed, so keep the centre of the design clear. |
| `settings.certificate.backgroundOverlay` | boolean | yes | Lay a semi-transparent white layer over the background image so text stays readable on busy designs; no effect without a background image. |
| `settings.comments` | object | no | — |
| `settings.comments.enabled` | boolean | yes | — |
| `settings.comments.threaded` | boolean | yes | — |
| `settings.comments.requireReview` | boolean | yes | — |
| `settings.comments.reactions` | boolean | yes | — |
| `settings.requireSequential` | boolean | no | Learners must complete each unit before opening the next (free-preview units are exempt). Locked units come back with access.reason prerequisite. |
| `settings.mediaCompletionPercent` | integer | no | Share of a video/audio unit a learner must actually watch for it to count as completed (50-100, step 5, default 90). Measured from the segments played, so skipping to the end does not count; 100 keeps a 2-second tolerance at the end. Custom players that only report positionSeconds are measured by the furthest position. Raising it never un-completes learners. |
| `fieldData` | object \| null | no | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `title` | string | yes | — |
| `description` | string \| null | yes | — |
| `coverAssetId` | string \| null | yes | — |
| `structure` | "COURSE" \| "DOWNLOAD" | yes | COURSE has chapters and tracks progress; DOWNLOAD is a flat file list (no progress, new units allow download by default). Access rules are identical. |
| `status` | "ACTIVE" \| "ARCHIVED" | yes | — |
| `grants` | string[] | yes | Commercial feature keys (e.g. ["premium"]) granted when a buyer purchases or is granted this deliverable; matched by ContentCollection.accessPolicy and entitlements.check. |
| `authorIds` | string[] | yes | Authors/instructors shown on the sales page and the learner page, in order. Ids come from the site's shared author list (also used by posts; managed in the console under Posts › Settings). |
| `unitCount` | integer | yes | Leaf units only; chapters are not counted. |
| `productIds` | string[] | yes | Products that grant this deliverable on purchase (see products.update deliverables). |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `settings` | object | yes | — |
| `settings.completionRule` | object | yes | — |
| `settings.completionRule.type` | "all_units" | yes | — |
| `settings.certificate` | object | yes | When enabled, a certificate (serial + issue date) is issued the first time a learner completes the deliverable; learners fetch it with Customer API GET /deliverables/:id/certificate. The PDF is generated on download from the current settings, course title, learner name and site branding (logo, brand colour), so edits apply to every certificate; only the serial and issue date are fixed. Send every certificate field when updating it. Use deliverables.certificate_preview to see it. |
| `settings.certificate.enabled` | boolean | yes | — |
| `settings.certificate.title` | string \| null | yes | Certificate heading; null = the site-language default ("Certificate of Completion"). |
| `settings.certificate.signatureName` | string \| null | yes | Signer's name or organisation; null = no signature block. |
| `settings.certificate.signatureTitle` | string \| null | yes | Signer's title printed under the name, e.g. "Lead instructor". |
| `settings.certificate.signatureAssetId` | string \| null | yes | Signature image: a public image asset id (same rule as coverAssetId); printed above the signature line. |
| `settings.certificate.showLogo` | boolean | yes | Print the site's brand logo (site settings) at the top; icon logos are skipped. |
| `settings.certificate.note` | string \| null | yes | One line printed under the course title, e.g. "12 hours of instruction". |
| `settings.certificate.backgroundAssetId` | string \| null | yes | Background image: a public image asset id (same rule as coverAssetId), centred and cropped to fill an A4 landscape page (recommended 3508 × 2480 px). When set, the default border is not drawn; text positions stay fixed, so keep the centre of the design clear. |
| `settings.certificate.backgroundOverlay` | boolean | yes | Lay a semi-transparent white layer over the background image so text stays readable on busy designs; no effect without a background image. |
| `settings.comments` | object | yes | — |
| `settings.comments.enabled` | boolean | yes | — |
| `settings.comments.threaded` | boolean | yes | — |
| `settings.comments.requireReview` | boolean | yes | — |
| `settings.comments.reactions` | boolean | yes | — |
| `settings.requireSequential` | boolean | yes | Learners must complete each unit before opening the next (free-preview units are exempt). Locked units come back with access.reason prerequisite. |
| `settings.mediaCompletionPercent` | integer | yes | Share of a video/audio unit a learner must actually watch for it to count as completed (50-100, step 5, default 90). Measured from the segments played, so skipping to the end does not count; 100 keeps a 2-second tolerance at the end. Custom players that only report positionSeconds are measured by the furthest position. Raising it never un-completes learners. |
| `fieldData` | object \| null | yes | — |
| `units` | object[] | yes | Flat list ordered by chapter then unit; use parentId to rebuild the tree. |
| `units[].id` | string | yes | — |
| `units[].parentId` | string \| null | yes | Chapter id for units inside a chapter; null for top-level chapters or flat units. |
| `units[].order` | integer | yes | — |
| `units[].kind` | string | yes | Open string. Known values: video, audio, file, text, quiz; "chapter" for grouping rows. Unknown kinds render as a download card. |
| `units[].title` | string \| null | yes | — |
| `units[].description` | string \| null | yes | — |
| `units[].primaryAssetId` | string \| null | yes | Media-library asset (private products bucket). Upload with media.upload first. |
| `units[].attachmentAssetIds` | string[] | yes | — |
| `units[].fileUrl` | string \| null | yes | Only for Kanorio template sample media (not a media-library asset). Real files are referenced by primaryAssetId. |
| `units[].embed` | object \| null | yes | External video embed (youtube/vimeo/loom). Not protected by Kanorio; use for FREE sites that cannot host video. |
| `units[].embed.provider` | string | yes | — |
| `units[].embed.id` | string | yes | — |
| `units[].body` | any | yes | text units: { markdown }. quiz units: { questions: [{ id, type: "single" \| "multiple" \| "true_false", prompt, options: [{ id, text, correct: boolean }], explanation? }], settings: { passingPercent (0-100, default 70), maxAttempts (1-99 or null = unlimited), showAnswers: "never" \| "after_pass" \| "always" } }; validated on write (UNIT_TREE_INVALID quiz_body_invalid). Learners never see correct/explanation; the server grades Customer API POST /units/:id/quiz. |
| `units[].isPreview` | boolean | yes | Free preview: anonymous visitors can open it. |
| `units[].visibility` | "VISIBLE" \| "HIDDEN" | yes | — |
| `units[].releaseAt` | string \| null | yes | Absolute unlock time. |
| `units[].releaseAfterDays` | integer \| null | yes | Unlock N days after the learner's entitlement started. |
| `units[].allowDownload` | boolean | yes | — |
| `units[].countsTowardCompletion` | boolean | yes | false = bonus unit: not counted in percent or completion, never locked by requireSequential and never a gate for later units. |
| `units[].isPrerequisite` | boolean | yes | Units after this one stay locked until the learner completes it, even when requireSequential is off. |
| `products` | object[] | yes | Products (variants) that grant this deliverable; tierId null = every variant. Access duration is pricingTiers[].accessDays on the product. |
| `products[].productId` | string | yes | — |
| `products[].tierId` | string \| null | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `DELIVERABLE_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | details.issues |
| `ASSET_NOT_FOUND` | 404 | details.assetIds: not in the media library, in the trash or an older version |
| `ASSET_NOT_ALLOWED` | 422 | coverAssetId, certificate.signatureAssetId or certificate.backgroundAssetId is not a public image |
| `AUTHOR_NOT_FOUND` | 404 | details.authorIds: not in the site's author list |

```bash
curl -X PATCH "https://app.kanorio.com/api/v1/sites/{id}/deliverables/{deliverableId}" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"settings":{"completionRule":{"type":"percent","percent":80},"certificate":{"enabled":true,"title":"Certificate of Completion","signatureName":"Head Baker","signatureTitle":"Lead instructor","signatureAssetId":null,"showLogo":true,"note":"12 hours of instruction","backgroundAssetId":"asset_example01","backgroundOverlay":true}}}'
```

```json
{
  "id": "dlv_example01",
  "title": "Sourdough Fundamentals",
  "description": "Six-week video course with downloadable recipe cards.",
  "coverAssetId": "asset_example01",
  "structure": "COURSE",
  "status": "ACTIVE",
  "grants": [
    "premium"
  ],
  "authorIds": [
    "author_headbaker01"
  ],
  "unitCount": 3,
  "productIds": [
    "prod_example01"
  ],
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "settings": {
    "completionRule": {
      "type": "percent",
      "percent": 80
    },
    "certificate": {
      "enabled": true,
      "title": "Certificate of Completion",
      "signatureName": "Head Baker",
      "signatureTitle": "Lead instructor",
      "signatureAssetId": null,
      "showLogo": true,
      "note": "12 hours of instruction",
      "backgroundAssetId": "asset_example01",
      "backgroundOverlay": true
    },
    "comments": {
      "enabled": false,
      "threaded": true,
      "requireReview": true,
      "reactions": false
    },
    "requireSequential": false,
    "mediaCompletionPercent": 90
  },
  "fieldData": null,
  "units": [
    {
      "id": "unit_chapter01",
      "parentId": null,
      "order": 0,
      "kind": "chapter",
      "title": "Week 1 · Getting started",
      "description": null,
      "primaryAssetId": null,
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": null,
      "isPreview": false,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": null,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    },
    {
      "id": "unit_video01",
      "parentId": "unit_chapter01",
      "order": 0,
      "kind": "video",
      "title": "Welcome and course overview",
      "description": "What you will build in six weeks.",
      "primaryAssetId": "asset_example01",
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": null,
      "isPreview": true,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": null,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    },
    {
      "id": "unit_text01",
      "parentId": "unit_chapter01",
      "order": 1,
      "kind": "text",
      "title": "Set up your workspace",
      "description": null,
      "primaryAssetId": null,
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": {
        "markdown": "## Install\n\n1. Download the starter kit…"
      },
      "isPreview": false,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": 2,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    }
  ],
  "products": [
    {
      "productId": "prod_example01",
      "tierId": null
    }
  ]
}
```

### Duplicate a deliverable

`POST /sites/{id}/deliverables/{deliverableId}/duplicate`

- Stability: beta
- Permission: `commerce:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_duplicate`

Copy a deliverable with its settings and the whole chapter/unit tree (files are shared, not copied). Product links, entitlements, learner progress and downloads are not copied: the copy starts with no learners. Pass a different structure to switch between COURSE and DOWNLOAD (the structure is fixed after creation); COURSE to DOWNLOAD drops the chapters and keeps their units in the same order. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND, VALIDATION (details.issues).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `deliverableId` | path | string | yes | Deliverable to copy |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | yes | Title of the copy |
| `structure` | "COURSE" \| "DOWNLOAD" | no | Structure of the copy; defaults to the source's. |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `title` | string | yes | — |
| `description` | string \| null | yes | — |
| `coverAssetId` | string \| null | yes | — |
| `structure` | "COURSE" \| "DOWNLOAD" | yes | COURSE has chapters and tracks progress; DOWNLOAD is a flat file list (no progress, new units allow download by default). Access rules are identical. |
| `status` | "ACTIVE" \| "ARCHIVED" | yes | — |
| `grants` | string[] | yes | Commercial feature keys (e.g. ["premium"]) granted when a buyer purchases or is granted this deliverable; matched by ContentCollection.accessPolicy and entitlements.check. |
| `authorIds` | string[] | yes | Authors/instructors shown on the sales page and the learner page, in order. Ids come from the site's shared author list (also used by posts; managed in the console under Posts › Settings). |
| `unitCount` | integer | yes | Leaf units only; chapters are not counted. |
| `productIds` | string[] | yes | Products that grant this deliverable on purchase (see products.update deliverables). |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |
| `settings` | object | yes | — |
| `settings.completionRule` | object | yes | — |
| `settings.completionRule.type` | "all_units" | yes | — |
| `settings.certificate` | object | yes | When enabled, a certificate (serial + issue date) is issued the first time a learner completes the deliverable; learners fetch it with Customer API GET /deliverables/:id/certificate. The PDF is generated on download from the current settings, course title, learner name and site branding (logo, brand colour), so edits apply to every certificate; only the serial and issue date are fixed. Send every certificate field when updating it. Use deliverables.certificate_preview to see it. |
| `settings.certificate.enabled` | boolean | yes | — |
| `settings.certificate.title` | string \| null | yes | Certificate heading; null = the site-language default ("Certificate of Completion"). |
| `settings.certificate.signatureName` | string \| null | yes | Signer's name or organisation; null = no signature block. |
| `settings.certificate.signatureTitle` | string \| null | yes | Signer's title printed under the name, e.g. "Lead instructor". |
| `settings.certificate.signatureAssetId` | string \| null | yes | Signature image: a public image asset id (same rule as coverAssetId); printed above the signature line. |
| `settings.certificate.showLogo` | boolean | yes | Print the site's brand logo (site settings) at the top; icon logos are skipped. |
| `settings.certificate.note` | string \| null | yes | One line printed under the course title, e.g. "12 hours of instruction". |
| `settings.certificate.backgroundAssetId` | string \| null | yes | Background image: a public image asset id (same rule as coverAssetId), centred and cropped to fill an A4 landscape page (recommended 3508 × 2480 px). When set, the default border is not drawn; text positions stay fixed, so keep the centre of the design clear. |
| `settings.certificate.backgroundOverlay` | boolean | yes | Lay a semi-transparent white layer over the background image so text stays readable on busy designs; no effect without a background image. |
| `settings.comments` | object | yes | — |
| `settings.comments.enabled` | boolean | yes | — |
| `settings.comments.threaded` | boolean | yes | — |
| `settings.comments.requireReview` | boolean | yes | — |
| `settings.comments.reactions` | boolean | yes | — |
| `settings.requireSequential` | boolean | yes | Learners must complete each unit before opening the next (free-preview units are exempt). Locked units come back with access.reason prerequisite. |
| `settings.mediaCompletionPercent` | integer | yes | Share of a video/audio unit a learner must actually watch for it to count as completed (50-100, step 5, default 90). Measured from the segments played, so skipping to the end does not count; 100 keeps a 2-second tolerance at the end. Custom players that only report positionSeconds are measured by the furthest position. Raising it never un-completes learners. |
| `fieldData` | object \| null | yes | — |
| `units` | object[] | yes | Flat list ordered by chapter then unit; use parentId to rebuild the tree. |
| `units[].id` | string | yes | — |
| `units[].parentId` | string \| null | yes | Chapter id for units inside a chapter; null for top-level chapters or flat units. |
| `units[].order` | integer | yes | — |
| `units[].kind` | string | yes | Open string. Known values: video, audio, file, text, quiz; "chapter" for grouping rows. Unknown kinds render as a download card. |
| `units[].title` | string \| null | yes | — |
| `units[].description` | string \| null | yes | — |
| `units[].primaryAssetId` | string \| null | yes | Media-library asset (private products bucket). Upload with media.upload first. |
| `units[].attachmentAssetIds` | string[] | yes | — |
| `units[].fileUrl` | string \| null | yes | Only for Kanorio template sample media (not a media-library asset). Real files are referenced by primaryAssetId. |
| `units[].embed` | object \| null | yes | External video embed (youtube/vimeo/loom). Not protected by Kanorio; use for FREE sites that cannot host video. |
| `units[].embed.provider` | string | yes | — |
| `units[].embed.id` | string | yes | — |
| `units[].body` | any | yes | text units: { markdown }. quiz units: { questions: [{ id, type: "single" \| "multiple" \| "true_false", prompt, options: [{ id, text, correct: boolean }], explanation? }], settings: { passingPercent (0-100, default 70), maxAttempts (1-99 or null = unlimited), showAnswers: "never" \| "after_pass" \| "always" } }; validated on write (UNIT_TREE_INVALID quiz_body_invalid). Learners never see correct/explanation; the server grades Customer API POST /units/:id/quiz. |
| `units[].isPreview` | boolean | yes | Free preview: anonymous visitors can open it. |
| `units[].visibility` | "VISIBLE" \| "HIDDEN" | yes | — |
| `units[].releaseAt` | string \| null | yes | Absolute unlock time. |
| `units[].releaseAfterDays` | integer \| null | yes | Unlock N days after the learner's entitlement started. |
| `units[].allowDownload` | boolean | yes | — |
| `units[].countsTowardCompletion` | boolean | yes | false = bonus unit: not counted in percent or completion, never locked by requireSequential and never a gate for later units. |
| `units[].isPrerequisite` | boolean | yes | Units after this one stay locked until the learner completes it, even when requireSequential is off. |
| `products` | object[] | yes | Products (variants) that grant this deliverable; tierId null = every variant. Access duration is pricingTiers[].accessDays on the product. |
| `products[].productId` | string | yes | — |
| `products[].tierId` | string \| null | yes | — |
| `duplicatedFrom` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `DELIVERABLE_NOT_FOUND` | 404 | — |
| `VALIDATION` | 400 | details.issues |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/deliverables/{deliverableId}/duplicate" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Sourdough Fundamentals (recipe cards)","structure":"DOWNLOAD"}'
```

```json
{
  "id": "dlv_example02",
  "title": "Sourdough Fundamentals (recipe cards)",
  "description": "Six-week video course with downloadable recipe cards.",
  "coverAssetId": "asset_example01",
  "structure": "DOWNLOAD",
  "status": "ACTIVE",
  "grants": [
    "premium"
  ],
  "authorIds": [
    "author_headbaker01"
  ],
  "unitCount": 2,
  "productIds": [],
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z",
  "settings": {
    "completionRule": {
      "type": "all_units"
    },
    "certificate": {
      "enabled": false,
      "title": null,
      "signatureName": null,
      "signatureTitle": null,
      "signatureAssetId": null,
      "showLogo": true,
      "note": null,
      "backgroundAssetId": null,
      "backgroundOverlay": false
    },
    "comments": {
      "enabled": false,
      "threaded": true,
      "requireReview": true,
      "reactions": false
    },
    "requireSequential": false,
    "mediaCompletionPercent": 90
  },
  "fieldData": null,
  "units": [
    {
      "id": "unit_video02",
      "parentId": null,
      "order": 0,
      "kind": "video",
      "title": "Welcome and course overview",
      "description": "What you will build in six weeks.",
      "primaryAssetId": "asset_example01",
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": null,
      "isPreview": true,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": null,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    },
    {
      "id": "unit_text02",
      "parentId": null,
      "order": 1,
      "kind": "text",
      "title": "Set up your workspace",
      "description": null,
      "primaryAssetId": null,
      "attachmentAssetIds": [],
      "fileUrl": null,
      "embed": null,
      "body": {
        "markdown": "## Install\n\n1. Download the starter kit…"
      },
      "isPreview": false,
      "visibility": "VISIBLE",
      "releaseAt": null,
      "releaseAfterDays": 2,
      "allowDownload": false,
      "countsTowardCompletion": true,
      "isPrerequisite": false
    }
  ],
  "products": [],
  "duplicatedFrom": "dlv_example01"
}
```

### Archive a deliverable

`POST /sites/{id}/deliverables/{deliverableId}/archive`

- Stability: beta
- Permission: `commerce:manage`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_archive`

Archive a deliverable: it can no longer be granted or sold, existing learners keep access until you revoke their entitlements. Deliverables are never deleted. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `deliverableId` | path | string | yes | Deliverable id. Get it from deliverables.list. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `title` | string | yes | — |
| `description` | string \| null | yes | — |
| `coverAssetId` | string \| null | yes | — |
| `structure` | "COURSE" \| "DOWNLOAD" | yes | COURSE has chapters and tracks progress; DOWNLOAD is a flat file list (no progress, new units allow download by default). Access rules are identical. |
| `status` | "ACTIVE" \| "ARCHIVED" | yes | — |
| `grants` | string[] | yes | Commercial feature keys (e.g. ["premium"]) granted when a buyer purchases or is granted this deliverable; matched by ContentCollection.accessPolicy and entitlements.check. |
| `authorIds` | string[] | yes | Authors/instructors shown on the sales page and the learner page, in order. Ids come from the site's shared author list (also used by posts; managed in the console under Posts › Settings). |
| `unitCount` | integer | yes | Leaf units only; chapters are not counted. |
| `productIds` | string[] | yes | Products that grant this deliverable on purchase (see products.update deliverables). |
| `createdAt` | string | yes | — |
| `updatedAt` | string | yes | — |

**Errors**

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

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/deliverables/{deliverableId}/archive" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "dlv_example01",
  "title": "Sourdough Fundamentals",
  "description": "Six-week video course with downloadable recipe cards.",
  "coverAssetId": "asset_example01",
  "structure": "COURSE",
  "status": "ARCHIVED",
  "grants": [
    "premium"
  ],
  "authorIds": [
    "author_headbaker01"
  ],
  "unitCount": 3,
  "productIds": [
    "prod_example01"
  ],
  "createdAt": "2026-10-01T02:30:00.000Z",
  "updatedAt": "2026-10-06T08:00:00.000Z"
}
```

### Certificate preview

`GET /sites/{id}/deliverables/{deliverableId}/certificate-preview`

- Stability: beta
- Permission: `store:view`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_certificate_preview`

Short-lived URL (10 minutes) of a sample certificate PDF for one course deliverable, rendered exactly like a learner's certificate from the saved settings and site branding, with a "Preview" watermark and no serial. Nothing is issued or stored. Pass learnerName to see how a long or non-Latin name fits. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND, NOT_A_COURSE (download packs have no certificates).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `deliverableId` | path | string | yes | Deliverable id. Get it from deliverables.list. |
| `learnerName` | query | string | no | Sample learner name; omitted = the site-language sample name. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `url` | string | yes | Open or download the PDF; no auth needed, expires with expiresAt. |
| `expiresIn` | integer | yes | Seconds until the URL stops working. |
| `expiresAt` | string | yes | ISO-8601 timestamp with timezone, e.g. 2026-10-05T12:34:56.789Z. |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `DELIVERABLE_NOT_FOUND` | 404 | — |
| `NOT_A_COURSE` | 422 | download packs have no certificates |

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/deliverables/{deliverableId}/certificate-preview?learnerName=Alex+Chen" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "url": "https://app.kanorio.com/api/certificates/eyJrIjoicCJ9.3f1c…",
  "expiresIn": 600,
  "expiresAt": "2026-10-06T08:00:00.000Z"
}
```

### Deliverable insights

`GET /sites/{id}/deliverable-insights`

- Stability: beta
- Permission: `store:view`
- Rate limit: default (120/minute per token)
- MCP tool: `deliverables_insights`

Learning insights for one course or all courses: current state (learner statuses, stalled count, completion rate, median days to complete, quiz pass rate, certificates, expiring learners) plus period activity (active learners, new starts, completions, hours watched, daily actives). With deliverableId: that course's outline (reached/completed per unit, biggest drop) and progress distribution; without it: one row per course for comparison. Numbers that do not apply are null, never 0 (e.g. quizPassRate when the course has no quiz). No learner names: use progress.list for individual learners. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND, VALIDATION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `deliverableId` | query | string | no | One course; omit for every course on the site. |
| `days` | query | integer | no | Number of calendar days ending today in the website's timezone (1 to 365, defaults to 30). Ignored when from and to are provided. |
| `from` | query | string | no | Start calendar date (YYYY-MM-DD) in the website's timezone, inclusive. Must be paired with to. |
| `to` | query | string | no | End calendar date (YYYY-MM-DD) in the website's timezone, inclusive, up to today. Must be paired with from. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `range` | object | yes | — |
| `range.from` | string | yes | First calendar date of the period (website timezone). |
| `range.to` | string | yes | Last calendar date of the period (website timezone). |
| `range.timezone` | string | yes | — |
| `current` | object | yes | — |
| `current.learners` | integer | yes | Contacts with an active entitlement (counted once per course). |
| `current.statuses` | object | yes | — |
| `current.statuses.notStarted` | integer | yes | — |
| `current.statuses.inProgress` | integer | yes | — |
| `current.statuses.completed` | integer | yes | — |
| `current.statuses.expiredIncomplete` | integer | yes | — |
| `current.stalled` | integer | yes | Not started 14 days after being granted, or no activity for 7 days with progress; learners idle more than 90 days are not counted. |
| `current.expiringIncomplete` | integer | yes | Access ends within 14 days and the course is not completed. |
| `current.completed` | integer | yes | — |
| `current.completionRate` | number \| null | yes | 0-1; null when there are no learners. |
| `current.medianDaysToComplete` | number \| null | yes | null until someone has completed. |
| `current.quizPassRate` | number \| null | yes | 0-1; null when there is no quiz or nobody has attempted one. |
| `current.certificates` | integer \| null | yes | Certificates issued; null when no course in scope issues certificates. |
| `period` | object | yes | — |
| `period.active` | integer | yes | Learners with any progress, playback or download in the period. |
| `period.newStarts` | integer | yes | — |
| `period.completes` | integer | yes | — |
| `period.watchedSeconds` | integer \| null | yes | Wall-clock playback in the period; null when there are no media units. |
| `period.dailyActive` | object[] | yes | — |
| `period.dailyActive[].date` | string | yes | — |
| `period.dailyActive[].active` | integer | yes | — |
| `outline` | object \| null | yes | Only with deliverableId. |
| `outline.units` | object[] | yes | — |
| `outline.units[].unitId` | string | yes | — |
| `outline.units[].title` | string | yes | — |
| `outline.units[].chapterTitle` | string \| null | yes | — |
| `outline.units[].kind` | string | yes | — |
| `outline.units[].countsTowardCompletion` | boolean | yes | — |
| `outline.units[].reached` | integer | yes | Learners who opened the unit (cumulative). |
| `outline.units[].completed` | integer | yes | — |
| `outline.units[].avgCoveredPercent` | number \| null | yes | Media units: mean share watched per learner, 0-100; otherwise null. |
| `outline.units[].duration` | number \| null | yes | — |
| `outline.biggestDropUnitId` | string \| null | yes | — |
| `distribution` | object | yes | — |
| `distribution.histogram` | any[] | yes | Learners by progress band: 0-25, 25-50, 50-75, 75-100 percent. |
| `distribution.typical` | object[] | yes | Median progress N days after being granted. |
| `distribution.typical[].day` | integer | yes | — |
| `distribution.typical[].medianPercent` | number \| null | yes | — |
| `distribution.typical[].sample` | integer | yes | — |
| `distribution.sample` | integer | yes | — |
| `courses` | object[] \| null | yes | One row per course; only without deliverableId. |

**Errors**

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

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/deliverable-insights?deliverableId=dlv_example01&days=30" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "range": {
    "from": "2026-09-07",
    "to": "2026-10-06",
    "timezone": "Asia/Taipei"
  },
  "current": {
    "learners": 60,
    "statuses": {
      "notStarted": 13,
      "inProgress": 35,
      "completed": 10,
      "expiredIncomplete": 2
    },
    "stalled": 23,
    "expiringIncomplete": 6,
    "completed": 10,
    "completionRate": 0.17,
    "medianDaysToComplete": 12,
    "quizPassRate": 0.8,
    "certificates": 10
  },
  "period": {
    "active": 41,
    "newStarts": 9,
    "completes": 4,
    "watchedSeconds": 98400,
    "dailyActive": [
      {
        "date": "2026-10-05",
        "active": 7
      },
      {
        "date": "2026-10-06",
        "active": 9
      }
    ]
  },
  "outline": {
    "units": [
      {
        "unitId": "unit_video01",
        "title": "Welcome",
        "chapterTitle": "Week 1 · Getting started",
        "kind": "video",
        "countsTowardCompletion": true,
        "reached": 47,
        "completed": 38,
        "avgCoveredPercent": 86,
        "duration": 612
      },
      {
        "unitId": "unit_text01",
        "title": "Reading list",
        "chapterTitle": "Week 1 · Getting started",
        "kind": "text",
        "countsTowardCompletion": true,
        "reached": 31,
        "completed": 24,
        "avgCoveredPercent": null,
        "duration": null
      }
    ],
    "biggestDropUnitId": "unit_text01"
  },
  "distribution": {
    "histogram": [
      18,
      14,
      13,
      15
    ],
    "typical": [
      {
        "day": 7,
        "medianPercent": 25,
        "sample": 52
      },
      {
        "day": 30,
        "medianPercent": 60,
        "sample": 31
      }
    ],
    "sample": 60
  },
  "courses": null
}
```
