API Conventions
The three API surfaces, versioning, response envelopes, idempotency, and validation — the agreements every endpoint follows.
The backend exposes one HTTP API with three audiences, all generated from a
shared DocumentBuilder (apps/backend/src/config/openapi-documents.ts).
Knowing which surface you're on tells you the auth model and the response shape.
The three surfaces
| Surface | Path | Auth | Envelope | Audience |
|---|---|---|---|---|
| Internal | /api/... | User JWT | { success, data, error, meta } | Web/mobile dashboards |
| Developer (v1) | /api/v1/... | API key | { data, meta } / { error } | Partner integrations |
| EMA | /api/ema/... | EMA token | EMA schema | Embedded Mini Apps |
Each surface has its own OpenAPI document and reference: Internal · Developer.
Versioning
Routing uses URI versioning under the global /api prefix:
/api/v1/tasks explicit version 1 (partner API)
/api/tasks version-neutral (default)The partner API is the one with an explicit version contract and a deprecation lifecycle — see Versioning & deprecation. The committed OpenAPI snapshot makes the developer surface diffable, which powers the API changelog.
Response envelopes
Developer (v1) — data plus metadata:
{
"data": { "id": "task_h4n2k9x...", "title": "..." },
"meta": {
"requestId": "req-2c91...",
"timestamp": "2026-06-17T12:00:00.000Z",
"pagination": { "total": 42, "page": 1, "limit": 5, "hasMore": true }
}
}Internal — adds an explicit success flag:
{ "success": true, "data": { }, "error": null, "meta": { } }Both shapes are produced by the TransformInterceptor — handlers return plain
objects and the envelope is applied centrally.
Errors
Exceptions are caught by the global filters and shaped consistently. Validation
failures (from the global ValidationPipe) return 400 with a flattened
details array:
{
"error": {
"message": "Validation failed",
"details": [{ "field": "dueDate", "message": "must be a valid date" }]
}
}5xx responses are reported to Sentry with the request's correlation ID.
Idempotency
Mutating requests may send an Idempotency-Key header (PRD-224). A replay of
the same key
returns the original result with an Idempotency-Replayed header instead of
performing the action twice; concurrent replays get a 409 with Retry-After.
POST /api/v1/tasks
Idempotency-Key: 2c91-4f7a-...Validation & DTOs
- Every input is a DTO class validated with
class-validator; the global pipe runs withwhitelist+forbidNonWhitelisted, so unknown fields are rejected. - IDs are validated with
@IsPrefixedId()to enforce the prefixed-nanoid shape. transform: truecoerces and instantiates nested DTOs.
Custom headers
The CORS policy allows a specific set of platform headers:
| Header | Purpose |
|---|---|
X-Org-Id | Target org (super-admin act-as-org only). |
X-Org-Role | Target role (super-admin only). |
X-Experience-Id | EMA/experience context. |
X-Acting-User-Id | Impersonation context. |
Idempotency-Key | Idempotent mutations. |
See Multi-tenancy for why X-Org-Id is
honored only for super-admins.