Developers · Errors & limits

Errors, rate limits and pagination.

One error shape across every route, a per-key rate limit that fails closed, and cursor-based pagination for every list endpoint.

The envelope

Error shape

{ error: string, code: string, requestId: string, fields?: Record<string, string> } — Every error is JSON with a human-readable error message, a stable machine code to branch on, and the requestId of the request. 422 validation failures add a fields map of field-path → message.

A plain error

HTTP/1.1 404 Not Found
X-Request-Id: req_3f0c2b1e-7a54-4e0e-9d4b-6a1f2c8d9e10

{
  "error": "Not found",
  "code": "not_found",
  "requestId": "req_3f0c2b1e-7a54-4e0e-9d4b-6a1f2c8d9e10"
}

A 422 validation failure

{
  "error": "Validation failed",
  "code": "invalid_request",
  "requestId": "req_8d41c7aa-52b9-4c53-8f0e-1b7e5d2a9c34",
  "fields": {
    "legalName": "Required",
    "lei": "Must be exactly 20 letters or digits"
  }
}

Every response, success or failure, carries an X-Request-Id header. Send your own X-Request-Id (8-128 characters of letters, digits, '.', '_', ':' or '-') and it is echoed back; otherwise one is generated. Error bodies repeat it as requestId. Quote it when contacting support — it is the key to the server-side log entry.

  • error is always a string, as before; code and requestId are additive members, so clients that only read error keep working.
  • 429 responses set Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers.
  • Entity 412 revision conflicts add currentRevision to the body and set an ETag of the current revision.
  • Idempotency-Key: required on entity create/update; optional on every other create (people, deals, documents, share classes, shareholdings, webhook subscriptions, webhook test). A retry with the same key and request returns the original response with Idempotency-Replayed: true; the same key with a different request is 409. Entity receipts are kept indefinitely; for the other endpoints a key is remembered for 24 hours and only a 2xx response is stored. Keys are scoped to the API key.
  • ETag / If-Match: every single-record GET returns an ETag, and PATCH and DELETE accept an optional If-Match (a quoted ETag or *); a stale value is 412 with the current ETag. Entities use their numeric revision and require If-Match on PATCH. For the other resources the ETag is derived from the stored updatedAt, so it detects changes made through the API and the app; a record with no updatedAt has no ETag and only * matches it. Properties and fund commitments are read-only and carry no ETag.
  • The webhook-subscription 409 limit error adds limit: 25.
  • Unexpected failures are 500 with code internal_error (previously a bare 502 "Bad Gateway"); only a positively identified transient datastore outage is 503 with code upstream_unavailable and Retry-After: 5.
  • Restricted records a key cannot access are reported as 404 Not found, indistinguishable from absent records.

Branch on these

Error codes

Every error body carries a stable machine code. Messages may be reworded over time; codes will not be renamed or repurposed.

invalid_request
400 / 422: malformed JSON, a bad header or cursor, or a body that fails validation (see fields).
unauthorized
401: missing, malformed, unknown, revoked or expired API key.
forbidden
403: the key lacks the scope or capability, or the plan does not include the API.
not_found
404: no such record in the key's workspace (restricted records the key cannot read look the same).
conflict
409: a conflicting record exists, a limit is reached, the record is still referenced, or an Idempotency-Key was reused with a different request (or is still in flight).
precondition_failed
412: If-Match does not match the record's current revision / ETag (an ETag header carries the current value; entities also add currentRevision to the body).
precondition_required
428: a required If-Match header is absent.
payload_too_large
413: the JSON body exceeds 1 MiB.
rate_limited
429: the key's rate limit is exhausted; honour Retry-After.
internal_error
500: an unexpected failure on our side. Retrying the same request is safe only for reads, or for writes sent with an Idempotency-Key.
service_unavailable
503: the API is not configured here, or a mode change is in progress. Retry-After is set when a retry is expected to succeed.
upstream_unavailable
503: a dependency (the datastore) is temporarily unavailable; retry with backoff.
person_referenced
409: a person cannot be hard-deleted because other records refer to them, or it cannot be confirmed that none do; archive the person instead. Nothing was changed.
document_retained
409: a document is retained evidence (category kyc, linked to a person, or KYC or compliance evidence), so it cannot be deleted and its category cannot be changed other than to kyc; archive the document instead. Nothing was changed.
record_retained
409: a register or governance record cannot be deleted: a shareholding held by a person (mark it historic instead), or a deal that has been submitted to the investment committee, is locked, completed or linked, or that other records refer to, or an entity that other records depend on (archive it instead). Nothing was changed.
erasure_not_available
501: erasure of a person's personal data is a controlled, retention-aware SEC-4 process and is not available through the API. Nothing was changed; archive the person to stop using the record.

Every condition

Status codes

Every distinct status/error/condition combination documented anywhere in the API, collapsed from the per-endpoint lists.

StatusCodeErrorWhen
400invalid_requestIdempotency-Key header is required.The Idempotency-Key header is absent.
400invalid_requestIdempotency-Key header is required. | If-Match must contain one numeric entity revision.The Idempotency-Key header is absent, or If-Match is present but not a single non-negative integer.
400invalid_requestIdempotency-Key must be 8-200 characters with no control characters or surrounding whitespace.An Idempotency-Key header is present but malformed.
400invalid_requestIf-Match must be "*" or a quoted ETag taken from a previous response.The If-Match header is present but is not * or a quoted ETag list.
400invalid_requestInvalid JSON body.The request body is not parseable JSON.
400invalid_requestInvalid pagination cursor.The cursor is malformed or was tampered with.
401unauthorizedUnauthorizedMissing, malformed, unknown, revoked, or expired API key (no detail is leaked about which).
403forbiddenForbidden: <reason>The key lacks the required scope, the workspace subscription is not an active Enterprise plan, or workspace access is suspended.
403forbiddenForbidden: manage_webhooks capability requiredThe key does not carry the manage_webhooks capability.
403forbiddenForbidden: restricted_records capability requiredThe key does not carry the restricted_records capability. The LP register has no open records, so nothing is returned rather than a filtered page.
403forbiddenForbidden: The restricted_records capability is required to change record access.restricted or viewerIds supplied without the capability.
403forbiddenOutbound webhooks require Enterprise.The workspace billing document is not an active/trialing Enterprise subscription.
404not_foundNot foundNo record with this id exists in the key's workspace (restricted records the key cannot access are indistinguishable from absent ones).
404not_foundNot foundThe entity does not exist, has no approved deletion request, or is a restricted record the key cannot access.
409conflictA workspace may have at most 25 webhook subscriptions.The 25-subscription cap is reached. Body carries limit: 25.
409conflictDocument is still referenced by governance records; archive it instead of deleting.A meeting (filed or linked) or a resolution still cites the document. The body adds references: [{ collection, ids }]. Nothing is changed.
409conflictEntity conflict | Entity limit reached. | Idempotency key conflicts with an earlier request.A conflicting entity exists, the free-tier entity cap is reached, or the Idempotency-Key was already used with a different request body.
409conflictIdempotency key conflicts with an earlier request. | A request with this Idempotency-Key is still being processed.The key was already used with a different request, or the first request with it is still running (Retry-After is set).
409document_retainedThe category of retained evidence (KYC or compliance evidence, or a document linked to a person) can only be changed to kyc. Archive the document instead (PATCH archived: true).The stored document is retained evidence (category "kyc", linked to a person, or KYC or compliance evidence) and the request sets a category other than kyc. Nothing is changed; archive the document instead.
409record_retainedThis deal cannot be deleted: it has been submitted to the investment committee, is locked, completed or linked, or other records refer to it.The deal holds governance or evidence (see the description). The body does not say which. Nothing is changed.
409document_retainedThis document is retained evidence (KYC or compliance evidence, or linked to a person) and cannot be deleted. Archive it instead (PATCH archived: true).The document has category "kyc", is linked to one or more people, or is KYC or compliance evidence. Nothing is changed; archive the document instead.
409record_retainedThis entity cannot be deleted because other records depend on it. Archive it instead.Other records depend on the entity. Nothing is deleted; a deletion already under way stops with the entity kept. Archive the entity instead.
409record_retainedThis entity cannot be deleted while a legal hold is in place.A legal hold exists in the workspace. Nothing is deleted; a deletion already under way stops with the entity kept.
409person_referencedThis person cannot be deleted because other records refer to them. Archive the person instead.Other records refer to the person, or the workspace holds records for which it cannot be confirmed that they do not (for example KYC records stored in nested lists, or more structure-chart links, provider records or deals than one request can check). The body is the same whatever the reason and adds archive: { method, path, body }, the PATCH that archives the person. No data is changed; archiving is the default for a referenced person.
409record_retainedThis shareholding is held by a person and is part of the register of members, so it cannot be deleted. Mark it historic instead (PATCH status: historic).The shareholding's holderType is person. Nothing is changed; mark it historic instead.
409conflictWebhook subscription is not deliverable.The stored subscription fails integrity validation (disabled, malformed URL/events/secret, or wrong workspace).
412precondition_failedEntity revision does not match.If-Match does not equal the stored revision. Body carries currentRevision; ETag header carries the current revision.
412precondition_failedThe resource has changed since it was read.If-Match does not match the record's current ETag (an ETag header with the current value is returned). A record with no updatedAt has no ETag, so only * matches it.
413payload_too_largeRequest body too large.The JSON body exceeds 1 MiB (1,048,576 bytes).
422invalid_requestValidation failedThe body is not a plain JSON object, contains unknown keys, or a field fails its schema; a fields map names each offending field.
422invalid_requestValidation failedUnknown keys, bad URL, unknown/duplicate events, or recordAccess: restricted without the capability — fields map names each.
422invalid_requestValidation failedUnknown keys, no updatable fields, or a field fails validation.
428precondition_requiredIf-Match header is required.The If-Match header is absent.
429rate_limitedRate limit exceededMore than 1,000 requests in the key's 15-minute window. Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers are set.
500internal_errorInternal server error.An unexpected server failure. It is logged against the requestId in the body and the X-Request-Id header; quote that id when contacting support.
501erasure_not_availableErasure of a person's personal data is not available through the API. It is a controlled, retention-aware process (SEC-4) that weighs legal holds and retention periods; ask the workspace administrator to raise an erasure request. To stop using a person's record now, archive it (PATCH status: archived).Always. Nothing is changed.
503service_unavailableEntity mutations are temporarily unavailable.The workspace's entity-mutation mode is transitioning (Retry-After: 5 is set).
503service_unavailableEntity state requires reconciliation.Persisted entity/receipt state failed integrity checks (logged for operators).
503upstream_unavailablePlan entitlement is temporarily unavailable.The workspace's subscription record could not be read. The request fails closed rather than granting access; retry shortly.
503service_unavailableThe public API is not configured in this environment.The deployment has no API_KEY_PEPPER or Firebase Admin credentials (the feature is dormant).
503upstream_unavailableThe service is temporarily unavailable. Retry shortly.The datastore reported a transient outage or timeout. Retry-After: 5 is set; retry with backoff.

Per key

Rate limits

1000 requests per 15 minutes, per API key. Per API key (a Firestore-transaction counter per key; in production an unavailable limiter fails CLOSED).

429 response
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1752570600
X-Request-Id: req_c0a8e1f2-9b3d-4a6e-8f17-2d5c4b9e7a01

{
  "error": "Rate limit exceeded",
  "code": "rate_limited",
  "requestId": "req_c0a8e1f2-9b3d-4a6e-8f17-2d5c4b9e7a01"
}

Every response, success or failure, carries the key’s current standing via Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.

List endpoints

Pagination

{ data: T[], nextCursor: string | null } — pass ?cursor= from the previous page to fetch the next.

Request

curl "https://app.alethiahq.com/api/v1/entities?limit=50&cursor=eyJvZmZzZXQiOjUwfQ" \
  -H "Authorization: Bearer tk_{workspaceId}_{secret}"

Response

{
  "data": ["...up to 50 records"],
  "nextCursor": "eyJvZmZzZXQiOjUwfQ"
}
  • Default page size 50, maximum 200.
  • Collections are ordered by document id (stable, deterministic).
  • Pass nextCursor back as ?cursor= to fetch the following page; null means the end.
  • Cursors are OPAQUE. For keys without restricted_records they are AES-256-GCM-encrypted tokens; tampering yields 400 "Invalid pagination cursor."
  • A page may contain fewer than limit items even when nextCursor is non-null (restricted records are filtered after paging).
  • The webhook-subscriptions list is not paginated (capped at 25).

Rate limits, and how to raise them.

Talk to us about your expected volume. Rate limits are per key, and a workspace can hold more than one.