Skip to main content

API Contracts

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

Companion: APPLICATION_LOGIC · 05 API Design


1. Conventions

  • Base path: /api/v1
  • Content type: application/json; charset=utf-8
  • Auth: Bearer JWT (issued by identity-service) via Authorization header. All requests MUST include X-Tenant-Id (enforced by gateway and re-validated here).
  • Correlation: X-Request-Id, X-Correlation-Id, OTel traceparent echoed in response.
  • Idempotency: Idempotency-Key header required for all unsafe methods (POST/PATCH/DELETE) that mutate state.
  • Errors: RFC 7807 problem+json.
  • Pagination: cursor-based (?cursor=…&limit=…, max 200, default 50).

2. Resource Summary

MethodPathDescriptionRole
POST/api/v1/assignmentsCreate (draft)compliance_admin
GET/api/v1/assignmentsListcompliance_admin, auditor
GET/api/v1/assignments/{id}Retrieve— (scoped)
PATCH/api/v1/assignments/{id}Update draftcompliance_admin
POST/api/v1/assignments/{id}/activateActivatecompliance_admin
POST/api/v1/assignments/{id}/pausePausecompliance_admin
POST/api/v1/assignments/{id}/resumeResumecompliance_admin
POST/api/v1/assignments/{id}/archiveArchivecompliance_admin
POST/api/v1/assignments/{id}/targetsAdd/remove targetscompliance_admin
GET/api/v1/assignments/{id}/windowsList windowsmanager, compliance_admin
GET/api/v1/assignments/compliance-reportReportauditor, compliance_admin
POST/api/v1/assignments/suggestAI suggested assignment (S5)compliance_admin
POST/api/v1/assignments/recommendAlias of /assignments/suggest (EP-15 backlog wording)compliance_admin
GET/api/v1/users/{userId}/windowsWindows for learnerlearner (self), manager
GET/api/v1/healthzLiveness
GET/api/v1/readyzReadiness (deps ok)

3. Representative Payloads

3.1 POST /api/v1/assignments

Request:

{
"title": { "en": "Fire Safety 2026", "ar": "..." },
"description": { "en": "Annual fire-safety compliance." },
"courseId": "crs_01JA…",
"courseVersionPolicy": "latest",
"targets": [
{ "kind": "dynamic_group", "groupId": "dg_nurses_all" },
{ "kind": "org_unit", "orgUnitId": "ou_hospital_root", "includeDescendants": true }
],
"rrule": "FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=15",
"startDate": "2026-01-15",
"dueOffset": "P30D",
"gracePeriod": "P7D",
"escalation": {
"steps": [
{ "level": 1, "trigger": "on_overdue",
"actions": [{ "kind": "notify_user", "channel": "email" }] },
{ "level": 2, "trigger": { "afterDueOffset": "P3D" },
"actions": [{ "kind": "notify_manager", "channel": "email" }] }
],
"maxLevel": 3
},
"reminderPolicy": {
"enabled": true,
"schedule": [
{ "kind": "relative_to_due", "offset": "P-7D" },
{ "kind": "relative_to_due", "offset": "P-1D" },
{ "kind": "on_due" }
],
"channel": "email",
"suppressIfInProgress": false
}
}

Response 201 Created:

{
"id": "asn_01JA9Y6H…",
"tenantId": "tnt_…",
"state": "draft",
"createdAt": "2026-04-15T10:22:31.102Z",
"_links": {
"self": { "href": "/api/v1/assignments/asn_01JA9Y6H…" },
"activate": { "href": "/api/v1/assignments/asn_01JA9Y6H…/activate", "method": "POST" }
}
}

3.2 POST /{id}/activate

Request: empty body.

Response 200 OK:

{
"id": "asn_…",
"state": "active",
"activatedAt": "2026-04-15T10:24:55.000Z",
"materializationScheduled": true,
"horizonUntil": "2026-07-14"
}

3.3 GET /{id}/windows

Query params: ?state=overdue&userId=usr_…&cursor=…&limit=100

Response:

{
"data": [
{
"id": "win_01JAA…",
"assignmentId": "asn_…",
"userId": "usr_…",
"occurrenceStart": "2026-01-15",
"dueAt": "2026-02-14T00:00:00.000Z",
"graceUntil": "2026-02-21T00:00:00.000Z",
"state": "overdue",
"escalationLevel": 1,
"remindersSent": 3,
"enrollmentId": "enr_…"
}
],
"pagination": { "nextCursor": "eyJpZC…", "limit": 100 }
}

3.4 GET /compliance-report

Query: ?assignmentId=…&orgUnitId=…&asOf=2026-04-15&format=json|csv

Response (json):

{
"asOf": "2026-04-15T00:00:00.000Z",
"summary": {
"totalWindows": 10234,
"open": 1200, "in_progress": 2100,
"completed": 6400, "overdue": 400, "closed_missed": 134
},
"byOrgUnit": [
{ "orgUnitId": "ou_er", "completed": 812, "overdue": 24, "closed_missed": 3, "complianceRate": 0.971 }
],
"_links": { "csv": { "href": "/api/v1/assignments/compliance-report?format=csv&…" } }
}

3.5 POST /{id}/targets

Request:

{
"add": [{ "kind": "user", "userId": "usr_new" }],
"remove": [{ "kind": "dynamic_group", "groupId": "dg_legacy" }]
}

Response 200:

{ "id": "asn_…", "targets": [ …updated… ] }

3.6 POST /api/v1/assignments/suggest (S5)

Request:

{
"context": {
"orgUnitId": "ou_er",
"complianceFramework": "JCI",
"lookbackDays": 365
}
}

Response:

{
"proposal": { /* a CreateAssignmentCommand body */ },
"aiProvenance": {
"model": "claude-sonnet-4-20250514",
"promptId": "assignment/suggest",
"promptVersion": "1.2.0",
"traceId": "01HXYZ…",
"cost": { "microUSD": 18200, "tokens": { "in": 1320, "out": 642 } }
},
"rationale": { "en": "ER nurses lack renewal for Infection Control…" }
}

3.7 Error Shape

{
"type": "https://errors.ghasi.com/assignment/invalid-rrule",
"title": "Invalid RRULE",
"status": 400,
"detail": "RRULE must specify FREQ",
"instance": "/api/v1/assignments",
"traceId": "01HXYZ…"
}

Canonical error types:

Type URI suffixHTTPRetry
assignment/invalid-rrule400no
assignment/invalid-target400no
assignment/version-not-found404no
assignment/not-found404no
assignment/state-conflict409no
assignment/version-mismatch409yes
assignment/dependency-unavailable503yes
assignment/rate-limited429yes
common/unauthorized401no
common/forbidden403no

4. OpenAPI

The full OpenAPI 3.1 doc lives at ./openapi/assignment-service.yaml and is served at /api/v1/openapi.json (production: auth-gated). CI fails if drift is detected against handler signatures.

5. Versioning

  • URL-versioned (/api/v1). Breaking changes → /api/v2 with min-12-month overlap.
  • Event versioning separate (.v1, .v2). See 04 Event-Driven.

6. Rate Limits

EndpointLimit
POST /assignments30 / min / tenant
POST /{id}/activate10 / min / tenant
POST /suggest6 / min / tenant, 100 / day / tenant
GET /compliance-report20 / min / tenant
GET /{id}/windows300 / min / tenant

Rate limits returned on every response as RateLimit-* headers (IETF draft).

7. HATEOAS

State-transition links returned where applicable (activate, pause, resume, archive, addTargets). Clients should follow rels rather than hard-code paths.