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.* 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
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.