# Entitlements · Management API

> Management API reference for entitlements: GET /sites/{id}/entitlements, POST /sites/{id}/entitlements, GET /sites/{id}/entitlements/check, POST /sites/{id}/entitlements/{entitlementId}/revoke.

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

`GET /sites/{id}/entitlements`

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

List entitlements (who can access which deliverable or featureKey) filtered by contact, deliverable or featureKey; includes revoked and expired rows. Newest first. Errors: FORBIDDEN, DELIVERABLE_NOT_FOUND, 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. |
| `contactId` | query | string | no | — |
| `deliverableId` | query | string | no | Deliverable id. Get it from deliverables.list. |
| `featureKey` | query | string | no | — |
| `activeOnly` | query | boolean | no | Only rows that are not revoked and not expired. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `data` | object[] | yes | — |
| `data[].id` | string | yes | — |
| `data[].contactId` | string | yes | — |
| `data[].deliverableId` | string \| null | yes | — |
| `data[].unitId` | string \| null | yes | — |
| `data[].featureKey` | string \| null | no | Commercial feature key for feature-only grants (no deliverable), e.g. "premium". For deliverable-backed entitlements the feature keys come from Deliverable.grants and this field is null. |
| `data[].tierId` | string \| null | yes | — |
| `data[].productId` | string \| null | yes | Source product snapshot when the entitlement came from an order. |
| `data[].source` | string | yes | ORDER_LINE, MANUAL_GRANT, PREPAID_REDEMPTION, FREE, EXTERNAL (payment collected outside Kanorio), GIFT. |
| `data[].sourceRef` | string \| null | yes | — |
| `data[].startsAt` | string \| null | yes | — |
| `data[].endsAt` | string \| null | yes | — |
| `data[].revokedAt` | string \| null | yes | — |
| `data[].revokeReason` | string \| null | yes | — |
| `data[].createdAt` | 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 | — |
| `DELIVERABLE_NOT_FOUND` | 404 | — |
| `INVALID_CURSOR` | 400 | — |

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

```json
{
  "data": [
    {
      "id": "ent_example01",
      "contactId": "contact_example01",
      "deliverableId": "dlv_example01",
      "unitId": null,
      "featureKey": null,
      "tierId": null,
      "productId": null,
      "source": "MANUAL_GRANT",
      "sourceRef": null,
      "startsAt": "2026-10-01T02:30:00.000Z",
      "endsAt": null,
      "revokedAt": null,
      "revokeReason": null,
      "createdAt": "2026-10-01T02:30:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### Grant an entitlement

`POST /sites/{id}/entitlements`

- Stability: beta
- Permission: `customers:manage`
- Rate limit: default (120/minute per token)
- Idempotent: send `Idempotency-Key`
- MCP tool: `entitlements_grant`

Grant a contact access to a deliverable and/or a commercial featureKey without a Kanorio purchase (gift, corporate seat, paid newsletter subscriber, or a payment collected by your own checkout). Idempotent: pass idempotencyKey to make retries return the same entitlement; with source EXTERNAL, sourceRef (your transaction id) also dedupes per deliverable, and a revoked external grant is never re-created by a replay. source EXTERNAL is only accepted from agent tokens (PAT / OAuth), never from a browser session; it requires sourceRef. Kanorio Checkout does not need to be enabled. Errors: FORBIDDEN, CONTACT_NOT_FOUND, DELIVERABLE_NOT_FOUND (archived deliverables cannot be granted), VALIDATION.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Website id. Get it from sites.list or from sites.create. |
| `Idempotency-Key` | header | string | no | Client-chosen key (1-200 chars). Retrying with the same key and the same body returns the original result; the same key with a different body is rejected with IDEMPOTENCY_CONFLICT (409). Keys expire after 24 hours. |

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `contactId` | string | yes | Get it from contacts.list or contacts.add. |
| `deliverableId` | string | no | Deliverable id. Provide deliverableId and/or featureKey. |
| `featureKey` | string | no | Commercial feature key (e.g. "premium") matched by collection accessPolicy and entitlements.check. |
| `endsAt` | string \| null | no | Access expiry; omit for permanent access. |
| `source` | "MANUAL_GRANT" \| "GIFT" \| "EXTERNAL" | no | Why access is granted. MANUAL_GRANT (default) or GIFT for site-owner grants; EXTERNAL when the payment was collected outside Kanorio (requires sourceRef; agent tokens only). |
| `sourceRef` | string | no | Your system's reference for this grant (e.g. external transaction id). Used for deduplication and shown in the audit trail. |
| `idempotencyKey` | string | no | Client-chosen key; the same key on the same site always returns the same entitlement. |

**Response** (201)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `contactId` | string | yes | — |
| `deliverableId` | string \| null | yes | — |
| `unitId` | string \| null | yes | — |
| `featureKey` | string \| null | no | Commercial feature key for feature-only grants (no deliverable), e.g. "premium". For deliverable-backed entitlements the feature keys come from Deliverable.grants and this field is null. |
| `tierId` | string \| null | yes | — |
| `productId` | string \| null | yes | Source product snapshot when the entitlement came from an order. |
| `source` | string | yes | ORDER_LINE, MANUAL_GRANT, PREPAID_REDEMPTION, FREE, EXTERNAL (payment collected outside Kanorio), GIFT. |
| `sourceRef` | string \| null | yes | — |
| `startsAt` | string \| null | yes | — |
| `endsAt` | string \| null | yes | — |
| `revokedAt` | string \| null | yes | — |
| `revokeReason` | string \| null | yes | — |
| `createdAt` | string | yes | — |

**Errors**

| Code | Status | When |
| --- | --- | --- |
| `FORBIDDEN` | 403 | — |
| `CONTACT_NOT_FOUND` | 404 | — |
| `DELIVERABLE_NOT_FOUND` | 404 | archived deliverables cannot be granted |
| `VALIDATION` | 400 | — |

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/entitlements" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"contactId":"contact_example01","deliverableId":"dlv_example01"}'
```

```json
{
  "id": "ent_example01",
  "contactId": "contact_example01",
  "deliverableId": "dlv_example01",
  "unitId": null,
  "featureKey": null,
  "tierId": null,
  "productId": null,
  "source": "MANUAL_GRANT",
  "sourceRef": null,
  "startsAt": "2026-10-01T02:30:00.000Z",
  "endsAt": null,
  "revokedAt": null,
  "revokeReason": null,
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```

### Entitlement check

`GET /sites/{id}/entitlements/check`

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

Check whether a contact can access a deliverable, a single unit, or a commercial featureKey right now (entitlement, free preview, release schedule, expiry). 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. |
| `contactId` | query | string | yes | — |
| `deliverableId` | query | string | no | Deliverable id. Get it from deliverables.list. |
| `unitId` | query | string | no | Unit id. Get it from deliverables.get. |
| `featureKey` | query | string | no | Commercial feature key (e.g. "premium") checked by ContentCollection.accessPolicy. |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `granted` | boolean | yes | — |
| `reason` | "entitlement" \| "preview" \| "none" \| "not_released" \| "expired" \| "hidden" \| "prerequisite" \| "not_found" | yes | prerequisite: blocked by the course's require-in-order rule (lockedByUnitId is the unit to finish first). |
| `availableAt` | string \| null | yes | — |
| `lockedByUnitId` | string \| null | yes | When reason is prerequisite: the unit the learner must complete first. |

**Errors**

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

```bash
curl -X GET "https://app.kanorio.com/api/v1/sites/{id}/entitlements/check?contactId=contact_example01&deliverableId=dlv_example01" \
  -H "Authorization: Bearer $KANORIO_TOKEN"
```

```json
{
  "granted": true,
  "reason": "entitlement",
  "availableAt": null,
  "lockedByUnitId": null
}
```

### Revoke an entitlement

`POST /sites/{id}/entitlements/{entitlementId}/revoke`

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

Revoke one entitlement (learner loses access immediately; the row is kept for audit). Errors: FORBIDDEN, ENTITLEMENT_NOT_FOUND.

**Parameters**

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

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `reason` | string | no | — |

**Response** (200)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | — |
| `contactId` | string | yes | — |
| `deliverableId` | string \| null | yes | — |
| `unitId` | string \| null | yes | — |
| `featureKey` | string \| null | no | Commercial feature key for feature-only grants (no deliverable), e.g. "premium". For deliverable-backed entitlements the feature keys come from Deliverable.grants and this field is null. |
| `tierId` | string \| null | yes | — |
| `productId` | string \| null | yes | Source product snapshot when the entitlement came from an order. |
| `source` | string | yes | ORDER_LINE, MANUAL_GRANT, PREPAID_REDEMPTION, FREE, EXTERNAL (payment collected outside Kanorio), GIFT. |
| `sourceRef` | string \| null | yes | — |
| `startsAt` | string \| null | yes | — |
| `endsAt` | string \| null | yes | — |
| `revokedAt` | string \| null | yes | — |
| `revokeReason` | string \| null | yes | — |
| `createdAt` | string | yes | — |

**Errors**

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

```bash
curl -X POST "https://app.kanorio.com/api/v1/sites/{id}/entitlements/{entitlementId}/revoke" \
  -H "Authorization: Bearer $KANORIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Corporate seat reassigned"}'
```

```json
{
  "id": "ent_example01",
  "contactId": "contact_example01",
  "deliverableId": "dlv_example01",
  "unitId": null,
  "featureKey": null,
  "tierId": null,
  "productId": null,
  "source": "MANUAL_GRANT",
  "sourceRef": null,
  "startsAt": "2026-10-01T02:30:00.000Z",
  "endsAt": null,
  "revokedAt": "2026-10-06T08:00:00.000Z",
  "revokeReason": "Corporate seat reassigned",
  "createdAt": "2026-10-01T02:30:00.000Z"
}
```
