Skip to main content

API Contracts

:::info Source Sourced from services/delivery-service/API_CONTRACTS.md in the documentation repo. :::

Companion: 05 API Design · APPLICATION_LOGIC · SECURITY_MODEL

All endpoints conform to the platform API conventions defined in doc 05: JSON envelope, cursor pagination, Idempotency-Key on writes, If-Match for optimistic concurrency, RFC 9457 error responses.

1. Base Path

/api/v1/play-sessions

1a. Shipped slice — A3 (checklist 5.3)

Reconciles Phase 3.7 with the implementation monorepo. Normative OpenAPI: Ghasi-edTech/services/delivery-service/openapi.json.

SurfaceShipped behaviour
play_sessionsAuthoritative aggregate: stateactive | paused | completed | failed | abandoned; JSON cursor (sequence index, module/lesson, enrollment, course version, etc.); version, attempt_number, started_at, last_activity_at; lamport; RLS on tenant_id. Migrations through 0005_play_session_lifecycle.sql.
POST /play-sessionsCreates row (active, version 1). Body: enrollmentId, courseVersionId, optional deviceId. Prefixed IDs must match OpenAPI (Crockford base32 segment 26 chars after enr_ / cv_ / dev_ / usr_).
PATCH /play-sessions/{id}/navigateCursor update: target next | jump | branch; bumps version and lamport; optional If-Match on aggregate version.
GET /play-sessions/lastMax lamport across aggregate and sync.mutations PlaySession; state, source (aggregate | sync), lifecycle fields when source is aggregate.
POST /play-sessions/{id}/resumeAggregate or sync required; 409 delivery.play_session.terminal if aggregate terminal; upserts active + merged cursor; event delivery.play_session.resumed.v1; 403 if sync.revoked_bundles blocks bundle.
J-11Journey: docs/frontend/journeys/player/J-11-learner-online-certificate.md + player spec §8. Server contract (no browser): Ghasi-edTech/services/delivery-service/test/integration/play-session-j11-contract.integration.spec.ts — start → navigate (next) → last → resume.

§3 below: §3.1 (start) and §3.2 (navigate) match the shipped routes; use OpenAPI + handlers for authoritative fields and error codes (examples use the platform envelope — minor field drift is possible). §3.3 onward (pause, complete, tutor, mount-offline, list, etc.) remain target until implemented; error tables there are aspirational where not wired to enrollment-service or manifest validation.

2. Authentication & Headers

All endpoints require:

  • Authorization: Bearer <jwt>
  • X-Tenant-Id: <tenantId> (must match JWT tid)
  • traceparent: <W3C trace context>
  • Idempotency-Key: <ulid> (write endpoints)
  • If-Match: "<version>" (PATCH endpoints)

3. Endpoints

3.1 Start Play Session

POST /api/v1/play-sessions

Request Body:

{
"enrollmentId": "enr_01H...",
"courseVersionId": "cv_01H...",
"deviceId": "dev_01H..."
}

Response: 201 Created

{
"data": {
"id": "pls_01H...",
"state": "active",
"attemptNumber": 1,
"cursor": {
"moduleId": "mod_01",
"lessonId": "les_01",
"blockId": null,
"sequenceIndex": 0
},
"startedAt": "2026-04-15T10:00:00Z",
"lastActivityAt": "2026-04-15T10:00:00Z",
"isOffline": false,
"version": 1
},
"meta": {
"requestId": "req_01H...",
"apiVersion": "v1.0",
"traceId": "00-..."
}
}

Errors:

StatusCodeCondition
403enrollment.revokedEnrollment is not active
404enrollment.not_foundEnrollment does not exist
409session.rate_limitedToo many session starts
422play_package.not_foundNo PlayPackage for courseVersionId

3.2 Navigate

PATCH /api/v1/play-sessions/{id}/navigate

Request Body:

{
"target": {
"type": "next"
}
}

Or for jump navigation:

{
"target": {
"type": "jump",
"targetModuleId": "mod_02",
"targetLessonId": "les_05"
}
}

Or for branch decisions:

{
"target": {
"type": "branch",
"branchChoice": "scenario_a"
}
}

Response: 200 OK

{
"data": {
"id": "pls_01H...",
"state": "active",
"cursor": {
"moduleId": "mod_01",
"lessonId": "les_02",
"blockId": null,
"sequenceIndex": 1
},
"lastActivityAt": "2026-04-15T10:05:00Z",
"version": 2
},
"meta": { "requestId": "req_01H...", "apiVersion": "v1.0", "traceId": "00-..." }
}

Errors:

StatusCodeCondition
403session.not_ownerJWT user does not own session
404session.not_foundSession ID invalid
409session.invalid_stateSession not active
409session.version_conflictIf-Match does not match current version
422navigation.unreachableTarget not reachable (prerequisite unmet)
422navigation.invalid_targetTarget node does not exist in manifest

3.3 Pause Session

POST /api/v1/play-sessions/{id}/pause

Request Body: None (empty JSON {} acceptable)

Response: 200 OK

{
"data": {
"id": "pls_01H...",
"state": "paused",
"cursor": { "moduleId": "mod_01", "lessonId": "les_02", "blockId": null },
"lastActivityAt": "2026-04-15T10:10:00Z",
"version": 3
},
"meta": { "..." }
}

3.4 Complete Session

POST /api/v1/play-sessions/{id}/complete

Request Body: None

Response: 200 OK

{
"data": {
"id": "pls_01H...",
"state": "completed",
"cursor": { "moduleId": "mod_03", "lessonId": "les_12", "blockId": null },
"endedAt": "2026-04-15T11:30:00Z",
"durationSeconds": 5400,
"version": 15
},
"meta": { "..." }
}

Errors:

StatusCodeCondition
422completion.requirements_unmetNot all required lessons/gates completed

3.5 Abandon Session

POST /api/v1/play-sessions/{id}/abandon

Request Body:

{
"reason": "user_initiated"
}

Response: 200 OK

{
"data": {
"id": "pls_01H...",
"state": "abandoned",
"endedAt": "2026-04-15T10:15:00Z",
"version": 4
},
"meta": { "..." }
}

3.6 Get Session State

GET /api/v1/play-sessions/{id}/state

Response: 200 OK

{
"data": {
"id": "pls_01H...",
"state": "active",
"attemptNumber": 1,
"cursor": {
"moduleId": "mod_01",
"lessonId": "les_03",
"blockId": "blk_07",
"sequenceIndex": 5
},
"startedAt": "2026-04-15T10:00:00Z",
"lastActivityAt": "2026-04-15T10:20:00Z",
"durationSeconds": 1200,
"assistantTurnsCount": 3,
"isOffline": false,
"version": 6
},
"meta": { "..." }
}

3.7 AI Tutor — Submit Turn

POST /api/v1/play-sessions/{id}/tutor/turn

Request Body:

{
"prompt": "Can you explain the concept of polymorphism in this lesson?"
}

Response: 202 Accepted

{
"data": {
"turnId": "trn_01H...",
"streamUrl": "/api/v1/play-sessions/pls_01H.../tutor/stream?turnId=trn_01H..."
},
"meta": { "..." }
}

3.8 AI Tutor — SSE Stream

GET /api/v1/play-sessions/{id}/tutor/stream?turnId=trn_01H...

SSE Event Stream:

event: token
data: {"text": "Poly", "index": 0}

event: token
data: {"text": "morphism", "index": 1}

event: token
data: {"text": " is", "index": 2}

...

event: done
data: {"turnId": "trn_01H...", "totalTokens": 342, "aiProvenance": {"model": "claude-haiku-4.5", "local": false}}

event: error
data: {"code": "ai.unavailable", "message": "AI service temporarily unavailable"}

Headers:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no

3.9 Mount Offline Bundle

POST /api/v1/play-sessions/{id}/mount-offline

Request Body:

{
"bundleId": "bnd_01H...",
"licenseEnvelope": "<JWS token>",
"bundleChecksum": "sha256-abc123..."
}

Response: 201 Created

{
"data": {
"mountId": "mnt_01H...",
"bundleId": "bnd_01H...",
"expiresAt": "2026-05-15T10:00:00Z",
"signatureValid": true
},
"meta": { "..." }
}

Errors:

StatusCodeCondition
403device.not_trustedDevice not registered or not trusted
403license.invalidLicense envelope signature verification failed
403license.expiredLicense envelope has expired
409bundle.already_mountedBundle already mounted on this device
422bundle.integrity_failedBundle checksum mismatch

3.10 Unmount Offline Bundle

POST /api/v1/play-sessions/{id}/unmount-offline

Request Body:

{
"mountId": "mnt_01H...",
"reason": "user_initiated"
}

Response: 200 OK

{
"data": {
"mountId": "mnt_01H...",
"unmountedAt": "2026-04-15T12:00:00Z",
"reason": "user_initiated"
},
"meta": { "..." }
}

4. Rate Limits

EndpointLimitWindowScope
POST /play-sessions51 minper user
PATCH /{id}/navigate1201 minper session
POST /{id}/tutor/turn301 hourper session
POST /{id}/mount-offline101 hourper device
GET /{id}/state601 minper user

5. Pagination

GET /api/v1/play-sessions (list user sessions) uses cursor pagination:

Query Parameters:

  • cursor — opaque cursor from previous response
  • limit — max 100, default 20
  • state — filter: active, paused, completed, abandoned
  • courseVersionId — filter by course version

6. OpenAPI Schema Reference

The full OpenAPI 3.1 specification is generated from NestJS decorators and published at:

/api/v1/docs (Swagger UI)
/api/v1/openapi.json (machine-readable)