# API Response Spec v1.0 ## 0. Scope & Goals - Envelope **always** for success & errors. - **Hybrid error codes**: tiny global set + domain-prefixed codes. - Reserve optional, ignorable **ui** hints with actions. - Support **cursor & offset** pagination, **LRO**, **bulk**, **idempotency**, **ETag**. - JSON keys use **camelCase**. --- ## 1. Envelope (all responses) **Content-Type:** `application/json; charset=utf-8` ```json { "ok": true, "code": "MACHINE_CODE", "data": {}, "error": null, "links": { "self": "/v1/…", "next": "/v1/…", "prev": "/v1/…", "first": "/v1/…", "last": "/v1/…" }, "ui": { "messageKey": "…", "messageFallback": "…", "severity": "info|success|warning|error", "presentation": "toast|banner|dialog|inline", "actions": [] }, "meta": { "requestId": "req_…", "schemaVersion": "1.0", "generatedAt": "2025-08-26T00:00:00Z", "traceId": "…", "spanId": "…", "locale": "en-IN", "etag": "W/\"…\"", "idempotencyKey": "…" } } ``` **Rules** - `ok`, `code`, `meta` **always present**. - Exactly one of `data` **or** `error` must be present. - Use **HTTP status** correctly (2xx success, 4xx client/domain error, 5xx server fault). --- ## 2 Codes (Hybrid Policy) ### 2.1 Global codes (small, canonical) ``` UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, CONFLICT, VALIDATION_FAILED, PRECONDITION_FAILED, FAILED_DEPENDENCY, RATE_LIMITED, TIMEOUT, SERVICE_UNAVAILABLE, INTERNAL_ERROR ``` ### 2.2 Domain codes (prefixed) - Format: `USER_*`, `ORDER_*`, `PAYMENT_*`, `INVENTORY_*`, etc. - Examples: `USER_EMAIL_TAKEN`, `ORDER_OUT_OF_STOCK`, `PAYMENT_AUTH_DECLINED`. - Naming rule (lint): `^[A-Z]+(_[A-Z]+)*
, ≤4 segments, must start with a prefix. **Governance** - Global list is centralized; domain lists live in each service README and must not overlap global names. --- ## 3 Success Shapes ### 3.1 Single resource ```json { "ok": true, "code": "USER_FETCHED", "data": { "id": "usr_123", "name": "Ada", "createdAt": "…" }, "links": { "self": "/v1/users/usr_123" }, "meta": { "requestId": "req_1", "schemaVersion": "1.0", "generatedAt": "…" } } ``` ### 3.2 Collections (pagination) #### Cursor mode (preferred) ```json { "ok": true, "code": "USERS_LISTED", "data": { "items": [ { "id": "usr_1" }, { "id": "usr_2" } ], "page": { "mode": "cursor", "cursor": "base64…", "nextCursor": "base64…", "size": 50 } }, "links": { "self": "/v1/users?cursor=…&size=50", "next": "/v1/users?cursor=…&size=50" }, "meta": { "requestId": "req_2", "schemaVersion": "1.0", "generatedAt": "…" } } ``` #### Offset mode ```json { "ok": true, "code": "USERS_LISTED", "data": { "items": [ { "id": "usr_1" }, { "id": "usr_2" } ], "page": { "mode": "offset", "offset": 200, "limit": 50, "hasMore": true } }, "links": { "self": "/v1/users?offset=200&limit=50", "next": "/v1/users?offset=250&limit=50" }, "meta": { "requestId": "req_3", "schemaVersion": "1.0", "generatedAt": "…" } } ``` > No totals. Choose one pagination mode per endpoint. --- ## 4 Errors (Problem Object embedded) **Content-Type:** `application/json` (you may also set `application/problem+json`). ```json { "ok": false, "code": "VALIDATION_FAILED", "error": { "type": "https://errors.example.com/validation/invalid-argument", "title": "Validation failed", "status": 422, "detail": "One or more fields are invalid.", "instance": "req_abc123", "code": "VALIDATION_FAILED", "errors": [ { "path": "email", "reason": "INVALID_FORMAT", "message": "Must be a valid email" }, { "path": "password", "reason": "TOO_SHORT", "min": 12 } ], "hint": "Fix the highlighted fields and retry.", "docsUrl": "https://docs.example.com/errors/validation", "supportUrl": "https://support.example.com/tickets/new", "retryAfterSeconds": 0 }, "ui": { "messageKey": "errors.validation_failed", "messageFallback": "Some details need your attention.", "severity": "warning", "presentation": "inline", "actions": [ { "type": "retry", "label": "Try again" }, { "type": "link", "label": "View limits", "href": "https://docs.example.com/rate-limits" } ] }, "meta": { "requestId": "req_abc123", "schemaVersion": "1.0", "generatedAt": "…" } } ``` **HTTP ↔ code guidance** - 401→`UNAUTHENTICATED`, 403→`FORBIDDEN`, 404→`NOT_FOUND`, 409→`CONFLICT`, 412→`PRECONDITION_FAILED`, 422→`VALIDATION_FAILED`, 429→`RATE_LIMITED`, 5xx→`INTERNAL_ERROR`/`SERVICE_UNAVAILABLE`/`TIMEOUT`. --- ## 5. Bulk Operations (partial success) ```json { "ok": true, "code": "BULK_RESULT", "data": { "successes": [ { "index": 0, "id": "usr_1" }, { "index": 2, "id": "usr_3" } ], "failures": [ { "index": 1, "error": { "type": "https://errors.example.com/validation/unique", "title": "Validation failed", "status": 422, "code": "VALIDATION_FAILED", "detail": "Email is already taken", "errors": [{ "path": "email", "reason": "UNIQUE" }] } } ] }, "meta": { "requestId": "req_bulk_1", "schemaVersion": "1.0", "generatedAt": "…" } } ``` --- ## 6. Reserved `ui` (success & error) ```json { "messageKey": "user.created", "messageFallback": "User created", "severity": "info|success|warning|error", "presentation": "toast|banner|dialog|inline", "actions": [ { "type": "link", "label": "Open profile", "href": "/me" }, { "type": "route", "label": "Open in app", "route": "Profile", "payload": { "id": "usr_123" } }, { "type": "retry", "label": "Try again" }, { "type": "copy", "label": "Copy ID", "copyText": "usr_123" }, { "type": "support", "label": "Contact support", "payload": { "category": "billing", "priority": "normal" } } ] } ``` **Notes** - Entire `ui` block is optional and ignorable. - Prefer `messageKey` for future localization; `messageFallback` for default text. --- ## 7. Long-Running Operations (LRO) **Start** ```json { "ok": true, "code": "OPERATION_ACCEPTED", "data": { "name": "operations/op_789", "done": false, "metadata": { "kind": "user.import", "submittedAt": "2025-08-26T09:05:00Z", "progress": { "completed": 0, "total": 1000 } } }, "meta": { "requestId": "req_lro_1", "schemaVersion": "1.0", "generatedAt": "…" } } ``` **Poll – success** ```json { "ok": true, "code": "OPERATION_SUCCEEDED", "data": { "name": "operations/op_789", "done": true, "response": { "imported": 995, "skipped": 5 } }, "meta": { "requestId": "req_lro_2", "schemaVersion": "1.0", "generatedAt": "…" } } ``` **Poll – error** ```json { "ok": false, "code": "OPERATION_FAILED", "error": { "type": "https://errors.example.com/job/failed", "title": "Job failed", "status": 500, "detail": "Worker crashed on shard 3" }, "meta": { "requestId": "req_lro_3", "schemaVersion": "1.0", "generatedAt": "…" } } ``` --- ## 8) Concurrency & Idempotency ### ETags - Writes return `meta.etag`. - Clients send `If-Match: <etag>`. - On mismatch: HTTP 412 + `code=PRECONDITION_FAILED` and problem `error`. ### Idempotency (selected endpoints) - Clients send `Idempotency-Key: <uuid>`. - Server echoes `meta.idempotencyKey`. - Replays return the **original** envelope. --- ## 9) Rate Limits Headers: ``` X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset Retry-After (when 429/503) ``` Body on 429 uses error pattern with `code=RATE_LIMITED` and may include `retryAfterSeconds`. --- ## 10) Security & Redaction - Don’t leak internals in `detail`/`errors`. - Keep 5xx messages generic; correlate via `meta.requestId`. - Apply PII masking by policy. --- ## 11) Versioning - `meta.schemaVersion` tracks **envelope** version (start at `"1.0"`). - Resource payload versions are independent (route or content negotiation). --- ## 12) Validation & Lint Rules - Envelope contains `ok`, `code`, and `meta{requestId,schemaVersion,generatedAt}`. - Exactly one of `data` or `error`. - If `error` present: must include `status` (matches HTTP), `title`, `type` URI. - Pagination: `data.page.mode` ∈ `{cursor, offset}` with corresponding fields. - `ui.severity` ∈ `{info, success, warning, error}`; `ui.presentation` ∈ `{toast, banner, dialog, inline}`. - `ui.actions[]` must follow type-specific required fields. --- # JSON Schemas (v1) ## schemas/envelope.v1.json ```json { "$id": "https://schema.example.com/envelope.v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["ok", "code", "meta"], "properties": { "ok": { "type": "boolean" }, "code": { "type": "string" }, "data": {}, "error": { "$ref": "https://schema.example.com/problem.v1.json" }, "links": { "type": "object", "properties": { "self": { "type": "string" }, "next": { "type": "string" }, "prev": { "type": "string" }, "first": { "type": "string" }, "last": { "type": "string" } }, "additionalProperties": false }, "ui": { "$ref": "https://schema.example.com/ui.v1.json" }, "meta": { "type": "object", "required": ["requestId", "schemaVersion", "generatedAt"], "properties": { "requestId": { "type": "string" }, "schemaVersion": { "type": "string" }, "generatedAt": { "type": "string", "format": "date-time" }, "traceId": { "type": "string" }, "spanId": { "type": "string" }, "locale": { "type": "string" }, "etag": { "type": "string" }, "idempotencyKey": { "type": "string" } }, "additionalProperties": true } }, "additionalProperties": false, "allOf": [ { "if": { "properties": { "ok": { "const": true } } }, "then": { "required": ["data"], "not": { "required": ["error"] } } }, { "if": { "properties": { "ok": { "const": false } } }, "then": { "required": ["error"], "not": { "required": ["data"] } } } ] } ``` ## schemas/problem.v1.json ```json { "$id": "https://schema.example.com/problem.v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["type", "title", "status"], "properties": { "type": { "type": "string", "format": "uri" }, "title": { "type": "string" }, "status": { "type": "integer" }, "detail": { "type": "string" }, "instance": { "type": "string" }, "code": { "type": "string" }, "errors": { "type": "array", "items": { "type": "object", "required": ["path", "reason"], "properties": { "path": { "type": "string" }, "reason": { "type": "string" }, "message": { "type": "string" } }, "additionalProperties": true } }, "hint": { "type": "string" }, "docsUrl": { "type": "string", "format": "uri" }, "supportUrl": { "type": "string", "format": "uri" }, "retryAfterSeconds": { "type": "integer", "minimum": 0 } }, "additionalProperties": true } ``` ## schemas/page.v1.json ```json { "$id": "https://schema.example.com/page.v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "oneOf": [ { "type": "object", "required": ["mode", "cursor", "size"], "properties": { "mode": { "const": "cursor" }, "cursor": { "type": "string" }, "nextCursor": { "type": ["string", "null"] }, "size": { "type": "integer", "minimum": 1 } }, "additionalProperties": false }, { "type": "object", "required": ["mode", "offset", "limit", "hasMore"], "properties": { "mode": { "const": "offset" }, "offset": { "type": "integer", "minimum": 0 }, "limit": { "type": "integer", "minimum": 1 }, "hasMore": { "type": "boolean" } }, "additionalProperties": false } ] } ``` ## schemas/ui.v1.json ```json { "$id": "https://schema.example.com/ui.v1.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "messageKey": { "type": "string" }, "messageFallback": { "type": "string" }, "severity": { "enum": ["info", "success", "warning", "error"] }, "presentation": { "enum": ["toast", "banner", "dialog", "inline"] }, "actions": { "type": "array", "items": { "oneOf": [ { "type": "object", "required": ["type", "label", "href"], "properties": { "type": { "const": "link" }, "label": { "type": "string" }, "href": { "type": "string" } }, "additionalProperties": false }, { "type": "object", "required": ["type", "label", "route"], "properties": { "type": { "const": "route" }, "label": { "type": "string" }, "route": { "type": "string" }, "payload": { "type": "object", "additionalProperties": true } }, "additionalProperties": false }, { "type": "object", "required": ["type", "label"], "properties": { "type": { "const": "retry" }, "label": { "type": "string" } }, "additionalProperties": false }, { "type": "object", "required": ["type", "label", "copyText"], "properties": { "type": { "const": "copy" }, "label": { "type": "string" }, "copyText": { "type": "string" } }, "additionalProperties": false }, { "type": "object", "required": ["type", "label"], "properties": { "type": { "const": "support" }, "label": { "type": "string" }, "payload": { "type": "object", "additionalProperties": true } }, "additionalProperties": false } ] } } }, "additionalProperties": false } ``` --- ## Appendix: Implementation Rules 1. Always return the envelope with `ok`, `code`, `meta{requestId,schemaVersion,generatedAt}`. 2. Exactly one of `data` or `error`. 3. On error, set HTTP 4xx/5xx and embed problem object under `error`. 4. Choose one pagination mode per list endpoint; document it. 5. If endpoint is idempotent, require `Idempotency-Key` and echo in `meta`. 6. Writes return `meta.etag`; honor `If-Match`. 7. `ui` is optional and ignorable by clients.