02 — API Design Standards
Status: ✅ Draft
Prerequisite: TD3, 01 — Service Decomposition
Next: 03 — API Contracts
Conventions that apply to every Core API endpoint, across all 15 services. Module teams read this once and understand how every Core API behaves — before reading individual contracts.
Everything here is confirmed from TD2 and TD3. Individual contracts in 03 — API Contracts inherit these standards — they do not restate them.
1. Base URL and Versioning
All Core services are accessed through a single platform URL. Modules never call individual service addresses.
https://core.miqaat.platform/v1/{service-path}
The v1 segment is the API version. It is part of the path, not a header.
Versioning rules (confirmed in TD3):
| Change type | Version bump | Module impact |
|---|---|---|
| New optional field added to response | MINOR | Safe — existing modules ignore new fields |
| New optional request parameter | MINOR | Safe — existing modules omit it |
| Removing a response field | MAJOR | Breaking — requires coordinated migration |
| Renaming a field | MAJOR | Breaking |
| Changing a field's data type | MAJOR | Breaking |
| Changing an endpoint path | MAJOR | Breaking |
| Removing an endpoint | MAJOR | Breaking |
| Changing error codes modules handle | MAJOR | Breaking |
A MAJOR version bump means the old version stays live alongside the new one until all modules have migrated. No engine breaks a module by bumping versions unilaterally.
2. Request Headers
Every call to Core requires these headers. The API Gateway validates them before routing.
Required on every request
| Header | Value | Set by |
|---|---|---|
Authorization | Bearer <ITS-token> | Module (user-initiated calls) |
X-Module-Id | Module identifier assigned by Platform Admin (e.g. vms, misaaq, prefix) | Module |
X-Request-Id | UUID generated per request — for tracing | Module |
Set by the Identity Bridge (not by the module)
The Identity Bridge at the Gateway validates credentials, strips them, and passes clean headers downstream to Core engines. Engines never see the raw token or API key.
| Header | Value | Notes |
|---|---|---|
X-Its-Id | Verified itsId extracted from the ITS token | Only present on user-initiated calls |
X-Correlation-Id | The module's X-Request-Id value, copied and renamed by the Bridge | Propagated through the full request chain — engine → internal engine calls → logs. Echoed back as requestId in the response envelope. |
Tracing chain: the module generates a UUID as X-Request-Id → the Bridge renames it to X-Correlation-Id and forwards it downstream → every engine includes it in its logs → if that engine calls another engine internally, it passes the same X-Correlation-Id forward → the engine echoes it back as requestId in the response. The module can surface this value to users so support can trace the full request path across all engines from a single ID. The value has no lifecycle beyond the request — it is not stored, only logged.
The two call types
User-initiated call — a human action triggers the Core call. The module includes the user's ITS bearer token.
POST /v1/rbac/check
Authorization: Bearer <ITS-token>
X-Module-Id: vms
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
After Identity Bridge processing, the engine receives:
X-Module-Id: vms
X-Its-Id: ITS-1234567
X-Correlation-Id: 550e8400-e29b-41d4-a716-446655440000
System-initiated call — an automated process calls Core with no human user (batch jobs, scheduled syncs, module-to-Core background processes). No ITS token is included.
POST /v1/allocation/request
Authorization: Bearer <API-key>
X-Module-Id: vms
X-Request-Id: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
Content-Type: application/json
After Identity Bridge processing, the engine receives:
X-Module-Id: vms
(no X-Its-Id)
X-Correlation-Id: 6ba7b810-9dad-11d1-80b4-00c04fd430c8
Endpoints that require a human context (permission checks, eligibility checks) reject system-initiated calls with AUTH_ITS_ID_REQUIRED. Endpoints that allow both must handle the absence of X-Its-Id gracefully.
3. Standard Response Envelope
Every Core API response — from every engine, whether FastAPI or Node.js — uses the same envelope. No engine returns a Python traceback, a Node.js stack dump, or a framework error page.
Success response
{
"status": "success",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"data": { }
}
status— always"success"on a 2xx responserequestId— theX-Correlation-Idvalue echoed back by the engine. This is the same UUID the module sent asX-Request-Id. Modules can log or surface this so users can quote it to support for end-to-end tracing.data— the engine-specific payload; structure defined per contract in 03 — API Contracts
Error response
{
"status": "error",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"error": {
"code": "RBAC_DENIED",
"message": "The specified itsId does not hold a role that grants this action.",
"details": { }
}
}
status— always"error"on a 4xx or 5xx responseerror.code— machine-readable constant; modules switch on this, not on the HTTP status codeerror.message— human-readable; for logging and support, not for display in module UIerror.details— optional; engine-specific additional context (e.g., which rule failed)
HTTP status codes
Core uses HTTP status codes as a coarse signal. The error.code is the precise signal.
| HTTP Status | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Resource created (write endpoints only) |
400 Bad Request | Request payload is malformed or fails validation |
401 Unauthorized | Missing or invalid Authorization header or API key |
403 Forbidden | Credentials valid but this module/user is not permitted to call this endpoint |
404 Not Found | The requested resource does not exist |
409 Conflict | The request conflicts with existing state (e.g., duplicate allocation request) |
422 Unprocessable Entity | Payload is syntactically valid but semantically rejected (e.g., missing required field in business logic) |
429 Too Many Requests | Rate limit exceeded |
500 Internal Server Error | Core engine error — the module should retry with backoff |
503 Service Unavailable | Engine is temporarily unavailable |
4. Standard Error Codes
Error codes are namespaced by engine. Modules switch on these constants.
Auth and gateway errors (all engines)
| Code | HTTP | Meaning |
|---|---|---|
AUTH_INVALID_API_KEY | 401 | API key missing or not recognised |
AUTH_API_KEY_REVOKED | 401 | API key has been revoked in Platform Admin |
AUTH_ITS_TOKEN_INVALID | 401 | ITS bearer token failed validation |
AUTH_ITS_TOKEN_EXPIRED | 401 | ITS bearer token has expired |
AUTH_ITS_ID_REQUIRED | 403 | Endpoint requires user context; system-initiated call rejected |
AUTH_MODULE_NOT_PERMITTED | 403 | This module's API key is not allowed to call this endpoint |
RATE_LIMIT_EXCEEDED | 429 | Module has exceeded its rate limit window |
RBAC Engine
| Code | HTTP | Meaning |
|---|---|---|
RBAC_DENIED | 200* | Permission check completed — result is denied |
RBAC_ROLE_NOT_FOUND | 404 | The specified role does not exist |
RBAC_INVALID_SCOPE | 400 | miqaatId scope is missing or unrecognised |
*
RBAC_DENIEDis returned as a200 OKwithdata.allowed: false— it is a valid business answer, not an error.
Eligibility Engine
| Code | HTTP | Meaning |
|---|---|---|
ELIGIBILITY_INELIGIBLE | 200* | Eligibility check completed — result is ineligible |
ELIGIBILITY_RULE_NOT_FOUND | 404 | The specified rule set does not exist for this miqaat |
ELIGIBILITY_DATA_UNAVAILABLE | 503 | A required data source (ITS sync, HR Bank) is temporarily unavailable |
* Same pattern — ineligible is a valid answer.
Config Cascade Engine
| Code | HTTP | Meaning |
|---|---|---|
CONFIG_NOT_FOUND | 404 | Key not set at any level of the cascade for this context |
CONFIG_INVALID_SCOPE | 400 | miqaatId or context level is unrecognised |
Allocation Engine
| Code | HTTP | Meaning |
|---|---|---|
ALLOCATION_FULL | 409 | No seats/slots remaining |
ALLOCATION_DUPLICATE | 409 | This itsId already has an allocation for this miqaat + category |
ALLOCATION_RULE_REJECTED | 422 | Rule Engine rejected the allocation request |
ALLOCATION_MIQAAT_CLOSED | 409 | Allocation phase for this miqaat is not open |
Miqaat Lifecycle Engine
| Code | HTTP | Meaning |
|---|---|---|
MIQAAT_NOT_FOUND | 404 | miqaatId does not exist |
MIQAAT_INVALID_TRANSITION | 422 | The requested phase transition is not valid from the current phase |
Platform errors (all engines)
| Code | HTTP | Meaning |
|---|---|---|
INTERNAL_ERROR | 500 | Unexpected engine error; retry with exponential backoff |
SERVICE_UNAVAILABLE | 503 | Engine temporarily unavailable; retry after Retry-After header value |
5. Pagination
List endpoints that return multiple items use cursor-based pagination. Offset pagination is not used — it produces inconsistent results under concurrent writes.
Request parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Number of items to return. Maximum 100. |
cursor | string | — | Opaque cursor from the previous response. Omit on the first page. |
Response structure (list endpoints)
{
"status": "success",
"requestId": "uuid",
"data": {
"items": [ ],
"pagination": {
"cursor": "eyJpZCI6IjEyMyJ9",
"hasMore": true,
"total": null
}
}
}
cursor— pass this as thecursorquery parameter to fetch the next pagehasMore—trueif there are more items beyond this pagetotal—nullby default; some endpoints provide a count where it is cheap to compute
6. Rate Limiting
The API Gateway enforces per-module rate limits. Limits are configured per module in Platform Admin — modules with predictable low-volume traffic have lower ceilings than modules with high-frequency use (e.g., Jamaat Scan calling RBAC on every scan event).
Rate limit headers (returned on every response)
| Header | Value |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When the limit is exceeded the gateway returns 429 Too Many Requests with error code RATE_LIMIT_EXCEEDED and a Retry-After header (seconds until the window resets).
Modules must implement exponential backoff on 429 responses. Retrying immediately is a protocol violation.
7. Idempotency
Mutation endpoints (allocations, role assignments, notifications) support idempotency keys to prevent duplicate operations caused by network retries.
How to use
Pass X-Idempotency-Key: <uuid> on any POST or PUT request to a mutation endpoint.
If Core has already processed a request with this key, it returns the original response — it does not re-execute the operation. Keys expire after 24 hours.
POST /v1/allocation/request
Authorization: Bearer <ITS-token>
X-Module-Id: vms
X-Request-Id: 550e8400-e29b-41d4-a716-446655440000
X-Idempotency-Key: 7b1f3c2a-4d5e-4f6a-8b9c-0d1e2f3a4b5c
Content-Type: application/json
If the response is 200 OK with the same requestId, the operation was a replay — not a new execution.
Endpoints that support idempotency are marked in 03 — API Contracts.
8. Health Endpoints
Every Core service exposes two health endpoints in the same format. Platform Admin, deployment tooling, and load balancers depend on these.
GET /health
Returns whether the service process is running. Used by load balancers and uptime monitors.
{
"status": "ok",
"service": "rbac-engine",
"version": "1.2.3"
}
Returns 200 OK if the process is alive. Returns 503 Service Unavailable if the process is degraded or shutting down.
GET /ready
Returns whether the service is ready to handle requests — data store connected, dependencies reachable. Used by deployment tooling before routing traffic to a new instance.
{
"status": "ready",
"service": "rbac-engine",
"checks": {
"database": "ok",
"config-cascade": "ok"
}
}
Returns 200 OK only when all checks pass. Returns 503 Service Unavailable if any dependency is unavailable. Deployment tools must wait for ready before routing traffic.
9. Read vs Write Endpoint Split
Confirmed in TD2 — module API keys are read-only. Write endpoints (those that mutate Core state) are restricted to Platform Admin and designated Core processes.
What this means in practice:
- Module teams only need to read the read-endpoint contracts in 03 — API Contracts.
- Write contracts exist but are not published for module teams — they are internal Platform Admin interfaces.
- The Gateway enforces this: a module API key calling a write endpoint receives
AUTH_MODULE_NOT_PERMITTED (403)before the request reaches the engine.
Write endpoints (examples — full list in 03 API Contracts):
POST /v1/miqaat— create a miqaatPOST /v1/miqaat/{id}/transition— advance miqaat phasePOST /v1/rbac/assign— assign a rolePUT /v1/config/set— set a config overridePUT /v1/rules/configure— update a rule definition
10. Content Type
All requests and responses use application/json. No other content type is supported on the Core API.
Content-Type: application/json
Accept: application/json
11. Null vs Absent Fields
Core follows a consistent rule:
- Absent field — the engine has no data for this field and the concept does not apply in this context
nullfield — the engine knows the field exists but the value is not set / not available yet
Module code must handle both. Do not treat an absent field and a null field as equivalent — they carry different semantic meaning.
12. Summary — What Every Engine Implements
This is the minimum contract every Core engine fulfils. Individual engines may add to this — they never subtract from it.
| Requirement | Detail |
|---|---|
| Base URL | https://core.miqaat.platform/v1/{path} |
| Versioning | Semantic versioning; MAJOR on breaking changes; old version stays live during migration |
| Required request headers | Authorization, X-Module-Id, X-Request-Id |
| Clean headers (set by Gateway) | X-Module-Id, X-Its-Id (user calls), X-Correlation-Id |
| Success envelope | { status, requestId, data } |
| Error envelope | { status, requestId, error: { code, message, details } } |
| No raw tracebacks in errors | Platform-level requirement; enforced at engine level |
| Health endpoints | GET /health · GET /ready on every service |
| Rate limit headers | X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset |
| Retry-After on 429 | Seconds until rate limit window resets |
| Idempotency (mutation endpoints) | X-Idempotency-Key header; 24-hour key expiry |
| Pagination (list endpoints) | Cursor-based; limit + cursor params; pagination in response |
| Read/write split | Module keys rejected on write endpoints at the Gateway |
| Content type | application/json only |