EmofyEmofy Developers

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.

codeHTTPMeaning
bad_request400Malformed request.
validation_error400Input failed validation (details lists fields).
invalid_cursor400Pagination cursor is invalid, expired, or foreign.
unauthorized401Missing or invalid credential.
forbidden403Authenticated but not permitted (scope/role/org).
not_found404Resource does not exist or is not visible.
conflict409Conflicts with current state.
idempotency_conflict409A request with this Idempotency-Key is still in flight.
idempotency_key_reused422Idempotency-Key already used with a different payload.
rate_limit_exceeded429Rate limit hit — see the Retry-After header.
internal_error500Unexpected 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: true response header. The handler does not run a second time.
  • Same payload, still in flight409 idempotency_conflict with a Retry-After: 1 header. Retry after the indicated delay.
  • Different payload422 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/5xx releases 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.
  • requestId nuance: a replayed body keeps the original meta.requestId (it is the cached response), while the X-Request-ID header 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 200 with Idempotency-Replayed: false and 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