POST /api/v1/lessons/{lessonId}/resources
Adds a resource to a lesson. Two shapes, and never both at once:
url;mediaId, which you get from
Upload a lesson resource. The
media must belong to this school; one from another school is a 404.storagePath
instead. It must lie in this lesson's folder and name a file that was
really uploaded: any other path is a 400, never attached.| Role in the organization | This endpoint |
|---|---|
OWNER | Allowed |
ADMIN | Allowed |
MEMBER | Refused |
| Requirement | Rule for this endpoint |
|---|---|
| Business role | None — a resource is attached by whoever may edit the lesson |
| Resource scope | Lesson of the school attached to the API key |
| Resource state | An archived program or course answers 403 |
Attaching writes two subjects — the resource and the lesson that carries it — and both permissions are required: granting one without the other would leave an orphan resource written by someone who cannot attach it.
| Parameter | Type | Required | Description |
|---|---|---|---|
lessonId | UUID | yes | Lesson visible from the key's school |
| Header | Required | Description |
|---|---|---|
x-api-key | yes | API key created for this school |
Idempotency-Key | yes | Stable key for this attachment |
X-Correlation-Id | no | Caller correlation id, returned or generated |
| Field | Type | Required | Nullable | Description |
|---|---|---|---|---|
resource.resourceType | string | yes | no | link or file |
resource.title | string | yes | no | 1 to 160 characters |
resource.url | string | for a link | yes | An https:// address, up to 2048 characters |
resource.mediaId | UUID | for a file, or storagePath | yes | The media returned by the upload endpoint. Takes precedence over storagePath |
resource.storagePath | string | for a file, or mediaId | yes | The path returned by the upload endpoint, inside this lesson's folder |
resource.mimeType | string | no | yes | MIME type of the stored file |
resource.sizeBytes | integer | no | yes | Size of the stored file |
{
"resource": {
"resourceType": "file",
"title": "Call grid",
"mediaId": "5b1f0c2e-0000-4000-8000-000000000000",
"mimeType": "application/pdf",
"sizeBytes": 20481
}
}curl -X POST "$COHORTE_API_BASE_URL/api/v1/lessons/$LESSON_ID/resources" \
-H "x-api-key: $COHORTE_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{"resource":{"resourceType":"link","title":"Call grid","url":"https://example.com/grid"}}'201 Created — the resource is attached, last in the list.200 OK — the same Idempotency-Key replayed the original attachment.400 Bad Request — missing Idempotency-Key, invalid JSON, a link without url, a file with neither mediaId nor storagePath, a storagePath outside this lesson's folder or with no uploaded file behind it, or a media that is not a stored file.401 Unauthorized — the key is missing, invalid, or no longer attached to a member.403 Forbidden — the bearer is not OWNER or ADMIN, or the program is archived.404 Not Found — the lesson is unknown or belongs to another school, or the mediaId is not a media of this school.The response carries the lesson. Read the lesson to get the resource list
with its ranks. A file resource's url is always the one of its media: a url
sent for a file is ignored.
{
"success": true,
"replayed": false,
"data": {
"id": "5d9f3a60-1c2b-4e7d-8f4a-6b7c8d9e0f1a",
"programId": "9f2c0a1e-4b7d-4c1a-9e3f-2a6b8c0d1e2f",
"courseId": "3b8e2d41-7a6c-4f1e-8b25-9c0d1e2f3a4b",
"sectionId": "c41e7b03-9d2a-4e6f-b1c8-5a6b7c8d9e0f",
"title": "Structure a discovery call",
"content": "## Why it matters\n\nA discovery call decides the rest of the sale.",
"videoUrl": "https://vimeo.com/912440188",
"status": "published",
"position": 1,
"createdAt": "2026-09-01T09:00:00.000Z",
"updatedAt": "2026-09-04T17:12:00.000Z"
},
"correlationId": "0f6b1d92-5c3e-4a8b-9d71-3e2f4a6b8c10"
}