Dociya

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):

Auth (all routes)10 / min / IP
Chat — send, stream, regenerate30 / min / household
Voice — one turn30 / min / household
Documents — presign, register, ask, extract-more, reprocess40 / min / household
Analytics ingest30 / min / IP

Webhooks

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.

Caution
Human-in-the-loop is structural, not a per-endpoint choice: chat's agent never executes a mutating action directly. It creates a pending action, returned in ChatResult.pendingActions, executed only via a separate POST /chat/actions/:id/approve call.