API Reference
Interactive API documentation for the Emofy Platform backend (OpenAPI/Swagger)
Browse and try the REST API endpoints. By default the spec is served from a
same-origin static snapshot (/openapi/developer.json), synced at build time
from the committed developer OpenAPI snapshot — so this reference renders without
a running backend.
Customising the spec source: set NEXT_PUBLIC_OPENAPI_SPEC_URL_DEVELOPER to
point Scalar at a live backend instead (e.g.
http://localhost:3006/api/docs/developer.json).
Response envelope (V1)
Every /api/v1/* endpoint returns the same envelope, so you can write one
response handler and reuse it everywhere. There is no success field.
A single resource:
{
"data": { "id": "task_01h...", "title": "..." },
"meta": { "requestId": "req-...", "timestamp": "2026-06-13T12:00:00.000Z" }
}A collection (paginated):
{
"data": [{ "id": "task_01h..." }],
"meta": {
"requestId": "req-...",
"timestamp": "2026-06-13T12:00:00.000Z",
"total": 42,
"page": 1,
"limit": 20,
"hasMore": true,
"pagination": { "total": 42, "page": 1, "limit": 20, "hasMore": true }
}
}Read pagination from meta.pagination — a single object you can hand to one
generic paginator component. The flat meta.total/page/limit/hasMore fields
mirror it for backward compatibility.
Pagination
Two modes share the same envelope. limit is capped at 100 per request.
Offset — ?page=2&limit=20. meta.pagination carries total, page,
limit, hasMore.
Cursor (keyset) — for large append-only collections (e.g. feed). Pass
?cursor=<value>&limit=50; the response's meta.pagination.nextCursor is the
token for the next page (null on the last page). When cursor is supplied,
page is ignored. Notes:
- The cursor is opaque — treat it as a black box; its encoding is not a documented contract and may change. Do not parse or construct it.
- A cursor is bound to the org that minted it. A cursor from another org, or a
malformed cursor, returns
400 invalid_cursor. - Cursor responses omit
total(counting a large collection on every page would be expensive). - A row deleted while you page is simply skipped — keyset pagination never duplicates or errors on it.
Error envelope (V1)
Errors use the error object; the HTTP status code matches error and there is
no success field. details is present only for validation errors.
{
"error": {
"code": "not_found",
"message": "Task not found",
"details": [{ "field": "title", "message": "title is required" }],
"requestId": "req-...",
"timestamp": "2026-06-13T12:00:00.000Z"
}
}Error codes
error.code comes from a documented, closed catalog — safe to switch on.
A code outside the catalog never leaks; the response falls back to the
status-based generic below.
code | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Malformed request. |
validation_error | 400 | Input failed validation (details lists fields). |
invalid_cursor | 400 | Pagination cursor is invalid, expired, or foreign. |
unauthorized | 401 | Missing or invalid credential. |
forbidden | 403 | Authenticated but not permitted (scope/role/org). |
not_found | 404 | Resource does not exist or is not visible. |
conflict | 409 | Conflicts with current state. |
idempotency_conflict | 409 | A request with this Idempotency-Key is still in flight. |
idempotency_key_reused | 422 | Idempotency-Key already used with a different payload. |
rate_limit_exceeded | 429 | Rate limit hit — see the Retry-After header. |
internal_error | 500 | Unexpected server error. |
The table above is a quick reference for the most common codes. The complete,
authoritative list — every generic, domain, family, and reserved code with its
HTTP status — is generated from the @emofy/types error-code SSOT:
→ Error Catalog · see also Errors for the full envelope.
Domain-specific codes (e.g. feed moderation, family) extend this set and are documented with their endpoints; they are always members of the same catalog.
Request correlation
Every response sets the X-Request-ID header. Its value is identical to
meta.requestId (success) or error.requestId (error). Send your own
X-Request-ID request header to trace a call end to end; the backend echoes it
back unchanged.
Idempotency
Mutating requests (POST, PATCH, DELETE) accept an optional
Idempotency-Key header so network retries never create duplicate resources.
Set it to a unique value per logical operation (a UUID is ideal): 1–255 visible
ASCII characters. GET requests ignore the header (they are already idempotent).
When you send the same key again:
- Same payload, already completed → the original response is replayed
byte-for-byte (same status code and body) with an
Idempotency-Replayed: trueresponse header. The handler does not run a second time. - Same payload, still in flight →
409 idempotency_conflictwith aRetry-After: 1header. Retry after the indicated delay. - Different payload →
422 idempotency_key_reused. Reusing one key for two different requests is a client bug; this surfaces it instead of silently replaying the wrong response.
Notes:
- Only successful (2xx) responses are stored. A
4xx/5xxreleases the key immediately, so a failed call can be safely retried with the same key. - Window: a stored response is replayable for 24 hours. After that the same key is treated as a first use.
- Scope: keys are scoped to your organization — your key never collides with another tenant's.
requestIdnuance: a replayed body keeps the originalmeta.requestId(it is the cached response), while theX-Request-IDheader reflects the current request. Use the header for tracing the retry, the body field for correlating with the original.- Large responses: if the original response body exceeded 256 KiB it is not
cached; a repeat returns
200withIdempotency-Replayed: falseand an empty body (the duplicate is acknowledged but no cached response is available).
# First call — creates the task, caches the response under "task-42".
curl -X POST https://api.emofy.net/api/v1/tasks \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: task-42" \
-H "Content-Type: application/json" \
-d '{"title":"Prepare report"}'
# Retry (e.g. after a timeout) — replays the SAME response, no second task.
curl -i -X POST https://api.emofy.net/api/v1/tasks \
-H "Authorization: Bearer $API_KEY" \
-H "Idempotency-Key: task-42" \
-H "Content-Type: application/json" \
-d '{"title":"Prepare report"}'
# → HTTP/1.1 201 Created
# → Idempotency-Replayed: true