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
Authorizationheader. All requests MUST includeX-Tenant-Id(enforced by gateway and re-validated here). - Correlation:
X-Request-Id,X-Correlation-Id, OTeltraceparentechoed in response. - Idempotency:
Idempotency-Keyheader 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
| Method | Path | Description | Role |
|---|---|---|---|
| POST | /api/v1/assignments | Create (draft) | compliance_admin |
| GET | /api/v1/assignments | List | compliance_admin, auditor |
| GET | /api/v1/assignments/{id} | Retrieve | — (scoped) |
| PATCH | /api/v1/assignments/{id} | Update draft | compliance_admin |
| POST | /api/v1/assignments/{id}/activate | Activate | compliance_admin |
| POST | /api/v1/assignments/{id}/pause | Pause | compliance_admin |
| POST | /api/v1/assignments/{id}/resume | Resume | compliance_admin |
| POST | /api/v1/assignments/{id}/archive | Archive | compliance_admin |
| POST | /api/v1/assignments/{id}/targets | Add/remove targets | compliance_admin |
| GET | /api/v1/assignments/{id}/windows | List windows | manager, compliance_admin |
| GET | /api/v1/assignments/compliance-report | Report | auditor, compliance_admin |
| POST | /api/v1/assignments/suggest | AI suggested assignment (S5) | compliance_admin |
| POST | /api/v1/assignments/recommend | Alias of /assignments/suggest (EP-15 backlog wording) | compliance_admin |
| GET | /api/v1/users/{userId}/windows | Windows for learner | learner (self), manager |
| GET | /api/v1/healthz | Liveness | — |
| GET | /api/v1/readyz | Readiness (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 suffix | HTTP | Retry |
|---|---|---|
assignment/invalid-rrule | 400 | no |
assignment/invalid-target | 400 | no |
assignment/version-not-found | 404 | no |
assignment/not-found | 404 | no |
assignment/state-conflict | 409 | no |
assignment/version-mismatch | 409 | yes |
assignment/dependency-unavailable | 503 | yes |
assignment/rate-limited | 429 | yes |
common/unauthorized | 401 | no |
common/forbidden | 403 | no |
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/v2with min-12-month overlap. - Event versioning separate (
.v1,.v2). See 04 Event-Driven.
6. Rate Limits
| Endpoint | Limit |
|---|---|
POST /assignments | 30 / min / tenant |
POST /{id}/activate | 10 / min / tenant |
POST /suggest | 6 / min / tenant, 100 / day / tenant |
GET /compliance-report | 20 / min / tenant |
GET /{id}/windows | 300 / 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.