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
{
"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,metaalways present.- Exactly one of
dataorerrormust 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
{
"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)
{
"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
{
"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).
{
"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)
{
"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)
{
"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
uiblock is optional and ignorable. - Prefer
messageKeyfor future localization;messageFallbackfor default text.
7. Long-Running Operations (LRO)
Start
{
"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
{
"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
{
"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_FAILEDand problemerror.
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.schemaVersiontracks envelope version (start at"1.0").- Resource payload versions are independent (route or content negotiation).
12) Validation & Lint Rules
- Envelope contains
ok,code, andmeta{requestId,schemaVersion,generatedAt}. - Exactly one of
dataorerror. - If
errorpresent: must includestatus(matches HTTP),title,typeURI. - 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
{
"$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
{
"$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
{
"$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
{
"$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
- Always return the envelope with
ok,code,meta{requestId,schemaVersion,generatedAt}. - Exactly one of
dataorerror. - On error, set HTTP 4xx/5xx and embed problem object under
error. - Choose one pagination mode per list endpoint; document it.
- If endpoint is idempotent, require
Idempotency-Keyand echo inmeta. - Writes return
meta.etag; honorIf-Match. uiis optional and ignorable by clients.