Errors & conventions
A handful of rules hold everywhere in this API. They aren't per-endpoint quirks — knowing them up front saves re-deriving them from behavior.
Error shape
There is no bespoke error envelope — the API returns Nest's default HttpException body. Request-validation failures (every mutating endpoint is zod-validated) come back as 400 with message as an array of "field: reason" strings.
{
"statusCode": 400,
"message": ["email: Invalid email", "password: String must contain at least 10 character(s)"],
"error": "Bad Request"
}A few endpoints return a typed body on a specific status instead of the generic shape — e.g. vault_full (402, billing limit) and voice_limit / voice_not_configured (402 / 503, on POST /voice/turn). Treat these as the exception, not the rule.
404, never 403
A document, deadline, conversation, or share that exists but belongs to a different household — or a malformed ID that can't possibly resolve to anything — both come back as a plain 404 Not Found. The API never returns 403 Forbidden: distinguishing "not yours" from "doesn't exist" would leak whether a given ID is real.
Masking & reveal
Sensitive fields (SSNs, full card/license numbers, dates of birth) are masked (•••• + last 4) in every list and detail response, in chat answers, and in logs. Seeing the real value requires a separate step-up-gated reveal call — see POST /documents/:id/fields/:key/reveal on the Documents page and step-up authentication.
Pagination
There mostly isn't any. Collections are household-scoped and small by product design, so almost every list endpoint returns its full result set — no cursor, no page param, no Link header. The two exceptions: GET /audit-log?limit= (default 50, hard cap 200) and chat's GET /chat/search?q= (fixed cap 30) / GET /chat/conversations (fixed cap 50).
Rate limits
Most reads and writes are unthrottled beyond the auth gate itself. The expensive paths — anything that calls an LLM or accepts an upload — are limited per household (falling back to per-IP for unauthenticated calls):
10 / min / IP30 / min / household30 / min / household40 / min / household30 / min / IPWebhooks
Two inbound webhooks exist — POST /billing/webhook (Stripe) and POST /ingestion/email/webhook (SendGrid Inbound Parse, for forward-to-Dociya email ingestion). Both are marked public — trust comes from signature verification (stripe-signature / HMAC-SHA256 over the raw body), not a bearer token.
ChatResult.pendingActions, executed only via a separate POST /chat/actions/:id/approve call.