Skip to content

Response envelope and errors

Every JSON response from the bAInquet API uses one wrapper, and every error code in this catalogue is documented at a stable URI so SDKs can map it to a typed error.

The envelope

One top-level boolean, ok, discriminates the union. ok: true carries data; ok: false carries error. Both branches carry meta, and both carry the ok field so one client code path handles both.

Success (Content-Type: application/json):

json
{
  "ok": true,
  "data": { "...": "typed payload" },
  "meta": { "requestId": "req_01J...", "cursor": "...", "limit": 25 }
}

Error (Content-Type: application/problem+json, RFC 9457 compatible):

json
{
  "ok": false,
  "error": {
    "code": "website.not_verified",
    "message": "Website must be verified before publishing.",
    "type": "https://docs.bainquet.online/errors/website.not_verified",
    "status": 409,
    "requestId": "c0ffee00-0000-4000-8000-000000000002",
    "retryable": false,
    "details": { "verificationState": "pending" }
  }
}

Branch on ok, not on HTTP status

A 200 can carry a partial-batch ingest result where individual items failed (see partial success). 204 No Content carries no body at all (no envelope), used for idempotent deletes and acks.

Success fields

FieldTypeNotes
okbooleanalways true on this branch
dataobject, array, or nullthe typed payload; an object for single-resource reads, an array for list reads (with meta.cursor), null only where a 200 action returns no body
metaobjectpresent on list responses and whenever requestId is surfaced

meta fields

FieldTypeNotes
requestIdstring (uuidv4)correlation id; echoes X-Request-Id or is server-minted; appears in logs and traces, and as error.requestId on the error branch
cursorstring or nullopaque next-page cursor; null means end of list; absent means the response is not a paginated collection
limitintegerpage size actually applied after clamping to the server max
totalinteger or nulloptional total count; null when a count is unavailable or expensive (cursor pagination does not guarantee total)

error fields

FieldTypeNotes
codestringnamespaced machine code from the catalogue; stable forever; SDKs map it to a typed error
messagestringhuman-readable English, safe to log; not localized and not for verbatim end-user display (localize off code)
typestring (URI)https://docs.bainquet.online/errors/{code}; the RFC 9457 type member
statusintegerHTTP status, mirrors the response status line
requestIdstring (uuidv4)same correlation id meta.requestId would carry
retryablebooleantrue means the client may retry the same request (honor Retry-After if present)
detailsobject or nullcode-specific structured context; never contains secrets, tokens, SQL, or stack traces

The error body is a superset of RFC 9457 problem+json: type, status, and a renamed detail (the envelope calls it message) map directly; code, details, retryable, and the top-level ok are RFC-permitted extensions. error.code is the primary programmatic key.

details shapes

error.details is code-specific but follows fixed shapes so SDKs can type it:

VariantUsed byShape
Field errorsvalidation.failed{ "fields": [ { "field": "<JSON path>", "code": "string", "message": "string" } ] }
Quotaquota.exceeded{ "limit": int, "used": int, "resetAt": "<ISO-8601>" }
Conflictpublish.conflict, ingest.duplicate{ "conflictingId": "uuid" }
Verificationwebsite.not_verified, verification.*`{ "verificationState": "pending
Sizeingest.payload_too_large{ "maxBytes": int, "gotBytes": int }
Plan gatebilling.payment_required{ "requiredPlan": "string" }
Emptyeverything else{} (emit {}, never null, when the code defines no context)

Pagination

Pagination is cursor-based only. The first page omits cursor; later pages echo back the opaque meta.cursor value verbatim.

GET /v1/websites/site_123/knowledge/search?q=price&limit=2&cursor=eyJrIjoiZmFjdF8wNDIifQ
Authorization: Bearer <USER_JWT>
json
{
  "ok": true,
  "data": [ { "...": "hit 1" }, { "...": "hit 2" } ],
  "meta": { "cursor": "eyJrIjoiZmFjdF8wNDQifQ", "limit": 2, "total": null, "requestId": "c0ffee00-...-0001" }
}
  • The cursor is opaque: a base64url server-signed keyset token, not an offset or a raw primary key. Treat it as a blob.
  • limit is clamped to a server max (default 50, max 200); the applied value comes back in meta.limit.
  • meta.cursor: null means the last page. A non-null cursor never appears on an empty data: [] page.
  • A cursor may become invalid after a retention or partition operation; reusing it then returns 422 validation.stale_cursor, and the client restarts from page one.

Partial success (batch semantics)

Transport and item outcome are separate layers. A batch that was received and processed returns ok: true (HTTP 200) even when some items failed; the per-item results live inside data, never as an envelope error. An ok: false body is reserved for envelope-level failure such as auth, a malformed body, or a whole-request rejection.

json
{
  "ok": true,
  "data": {
    "accepted": 8,
    "skipped": 1,
    "errored": 1,
    "results": [
      { "id": "p_1", "status": "accepted" },
      { "id": "p_2", "status": "skipped", "reason": "idempotent_replay" },
      { "id": "p_3", "status": "error", "code": "validation.failed",
        "details": { "fields": [ { "field": "url", "code": "format", "message": "invalid URL" } ] } }
    ]
  },
  "meta": { "requestId": "c0ffee00-...-0005" }
}

Per-item status is accepted, skipped, or error. A skipped item carries a reason of idempotent_replay, unchanged_checksum, or tombstoned. An error item reuses a catalogue code. See Ingestion and signing for the full batch flow.

Error-code catalogue

Codes are namespaced, additive-only, and immutable: a code's meaning, HTTP status, and retryable default never change once shipped. The authoritative registry is packages/shared/src/error-codes.ts; throwing AppError("code", ...) derives the status and retryable flag from that map, so a code not in the map cannot be emitted. The tables below reproduce the registry as shipped in contractVersion 0.4.0 (the 2026-06-15 reconciliation added the precise auth, ingest, connector, validation, and billing codes that the auth, ingestion, and connector specs already referenced).

auth.*

CodeHTTPRetryableMeaning
auth.unauthenticated401nono or invalid principal credential
auth.invalid_credentials401nowrong email or password on login
auth.token_expired401noaccess JWT or connector token expired; refresh
auth.token_revoked401norefresh-rotation reuse or revocation; re-login
auth.refresh_invalid401norefresh token not recognized
auth.refresh_reuse_detected401noa rotated refresh token was replayed
auth.session_revoked401nosession ended (logout-all, password change)
auth.token_version_stale401notoken issued before a tokenVersion bump
auth.invalid_signature401noconnector HMAC X-Signature mismatch
auth.timestamp_skew401yesX-Timestamp outside the +/-300s window; re-sign
auth.replay_detected409noa previously seen nonce was replayed
auth.mfa_required401nosecond factor required (2FA, see note)
auth.mfa_invalid401nosecond-factor code rejected (2FA, see note)
auth.email_unverified403noaction needs a verified email
auth.email_taken409noregistration email already in use
auth.forbidden403noauthenticated but RBAC role lacks permission
auth.forbidden_role403norole below the required rung
auth.forbidden_scope403nocredential used outside its scope
auth.scope_violation403noconnector or key used outside org/website/sourceType scope
auth.tenant_mismatch403nocross-tenant access blocked
auth.principal_role_misuse400nowrong principal type for the route
auth.service_credential_external401nointernal service credential used externally
auth.reauth_required401nostep-up re-authentication required
auth.google_unconfigured503noGoogle sign-in not configured on this deployment

2FA codes

auth.mfa_required (the account has 2FA enabled and no code was supplied) and auth.mfa_invalid (the code or recovery code was wrong) are returned by the live /v1/auth/2fa/* flow and the login second-factor gate.

validation.*

CodeHTTPRetryableMeaning
validation.failed422noone or more fields invalid; details.fields[] populated
validation.malformed_body400nobody is not valid JSON or wrong content-type
validation.unsupported_field422nounknown or forbidden field for this resource
validation.missing_idempotency_key400noa mutating ingest call lacks Idempotency-Key
validation.stale_cursor422nopagination cursor no longer valid; restart from page one
validation.scope_unknown422norequested scope is not a known scope
validation.scope_forbidden422norequested scope not permitted for this principal
validation.ambiguous_credential400nomore than one credential presented

website.* and verification.*

CodeHTTPRetryableMeaning
website.not_found404nounknown website id, or not visible to the principal
website.not_verified409noaction requires a verified website
verification.in_grace409yesverification in grace; retry after a re-check
verification.failed409noverification state failed or revoked
verification.method_unsupported422norequested verification method tier not available

connector.* and ingest.*

CodeHTTPRetryableMeaning
connector.unknown_type422noX-Connector-Type not registered
connector.version_unsupported426noX-Connector-Version below the minimum supported
connector.token_revoked403noconnector token revoked
connector.scope_violation403noconnector used outside its bound scope
connector.replay_detected409noconnector nonce replayed
connector.rotation_in_progress409yesa token rotation is in flight; retry shortly
ingest.payload_too_large413nobatch or item exceeds the per-tenant size cap
ingest.batch_too_large413nobatch exceeds the item-count cap (500)
ingest.duplicate409noidempotency replay with a conflicting body for the same key
ingest.idempotency_conflict409noidempotency key reused with different content
ingest.item_rejected422nowhole-request item rejection (per-item failures live in data)
ingest.item_invalid422noa single item failed item-level validation
ingest.unknown_item404noreferenced item id does not exist
ingest.batch_rejected422nothe whole batch was rejected (partial: false)
ingest.schema_conflict409noPOST /v1/ingest/schema mapping conflicts with an existing one
ingest.schema_invalid400noschema-advertisement body is invalid

quota.* and billing.*

CodeHTTPRetryableMeaning
quota.exceeded429yesplan limit hit; details:{limit,used,resetAt}; Retry-After set
quota.rate_limited429yesper-tenant or global abuse rate limit; Retry-After set
billing.payment_required402nofeature gated to a paid plan; details.requiredPlan
billing.plan_required402noaction requires a higher plan
billing.subscription_inactive402noorg subscription lapsed or canceled
billing.unconfigured503nopayments not wired on this deployment (see note)

Billing

The entitlements and usage reads are live (plan-derived constants). Live payment processing is wired at launch; in the current build a checkout attempt surfaces billing.unconfigured (503), framed as a server-config gap rather than a card failure. See REST API.

publish.*, admin.*, and generic

CodeHTTPRetryableMeaning
publish.conflict409yesconcurrent publish of the same node; details.conflictingId
publish.nothing_to_publish409nono derived node content to publish
publish.review_required409nounreviewed required-importance facts remain; details.pendingCount
admin.action_forbidden403noaction restricted to admin or super_admin
internal.error500nounhandled server fault; requestId is the only safe handle
service.unavailable503yesa dependency (queue, store) is temporarily down; Retry-After set

privacy.*

GDPR and takedown intake returns 202 Accepted on the happy path; these codes cover the error edges. Note that organization-level access control reuses auth.* and privacy.*; there is no organization.* namespace.

CodeHTTPRetryableMeaning
privacy.erasure_forbidden403nocaller is not the data subject or lacks organization_owner
privacy.erasure_in_progress409yesan erasure for this subject is already running; the request collapses onto it
privacy.subject_not_found404nono matching data subject or scope (existence not leaked)
privacy.takedown_forbidden403notakedown requested by a principal lacking authority over the target
privacy.takedown_target_not_found404notakedown target does not exist or is not visible
privacy.consent_required403noaction needs a lawful-basis or consent record that is absent

Branch on the registry codes

Earlier spec prose referenced a few code names that the server never emitted (for example auth.signature_invalid and ingest.item_invalid as a body code). The 2026-06-15 registry reconciliation resolved this: the codes above are exactly what the server can throw. Branch on these, not on names from older drafts.

Producer guarantees

  • Exactly one of data or error is present, matched to ok.
  • error.code always exists in this catalogue; error.status equals the HTTP status line; error.type is the canonical docs URI for the code.
  • requestId is present on every response (success meta.requestId or error.requestId).
  • A Retry-After header accompanies any retryable: true 429 or 503.
  • Secrets, PII, SQL, and stack traces never appear in message or details.

Consumers must tolerate unknown meta, error, and details fields for forward-compatibility: adding a code or an optional field is a minor, non-breaking change.

Owner-controlled structured data for AI.