REST API
A guided tour of the non-ingestion REST surface: the routes that are actually mounted in the Koa app, grouped by module, with the auth principal and role each enforces.
The ingestion surface (POST /v1/ingest/*) uses a different principal and a request-signing scheme; it has its own page, Ingestion and signing. The machine-readable contract is the interactive reference and the downloadable openapi.yaml.
Base URL and envelope
Production base URL is https://api.bainquet.online/v1. Every response uses the standard envelope; clients branch on ok, not on HTTP status. List reads serialize data as a bare array with the next-page cursor in meta.cursor. 204 No Content carries no body. See Envelope and errors for the full shape and the error-code catalogue.
Auth principals and roles
| Principal | How it authenticates |
|---|---|
| User | Authorization: Bearer <access JWT> (EdDSA/RS256, ~15 min), refreshed via the bnq_refresh httpOnly cookie |
| Connector | scoped bearer + HMAC (Ingestion and signing) |
| API key | bnq_live_... via Authorization: Bearer or X-Api-Key; created under /v1/organizations/{id}/api-keys |
Per-route enforcement happens inside handlers via requireUser and requireRole; the service layer additionally derives the effective per-organization role and enforces tenant scope. The RBAC ladder, low to high, is viewer < editor < organization_admin < organization_owner < admin < super_admin.
System probes Stable
| Method | Path | Auth | Notes |
|---|---|---|---|
| GET | /healthz | none | liveness, no I/O |
| GET | /readyz | none | readiness; round-trips Postgres; 503 service.unavailable if not ready |
| GET | /metrics | none | Prometheus-format metrics, plain text (not the JSON envelope) |
| GET | / | none | service banner |
Auth (/v1/auth)
Fully implemented against a Postgres repo.
| Method | Path | Principal | Status |
|---|---|---|---|
| POST | /v1/auth/register | anonymous | 201 |
| POST | /v1/auth/verify-email | anonymous | 200 |
| POST | /v1/auth/login | anonymous | 200 (sets bnq_refresh cookie) |
| POST | /v1/auth/google | anonymous | 200 (popup ID-token sign-in) |
| POST | /v1/auth/refresh | refresh cookie or body | 200 (rotated) |
| POST | /v1/auth/logout | user | 204 |
| POST | /v1/auth/logout-all | user | 204 |
| GET | /v1/auth/me | user | 200 |
| POST | /v1/auth/password/change | user | 204 (bumps token version, revokes sessions) |
| POST | /v1/auth/password/forgot | anonymous | 202 (always, no account enumeration) |
| POST | /v1/auth/password/reset | reset token | 204 |
POST /v1/auth/google verifies a Google ID token server-side and issues a bAInquet session with the same shape as login.
Two-factor authentication is available: GET /v1/auth/2fa (status), POST /v1/auth/2fa/setup (begin TOTP enrollment), /activate (verify a code and receive one-time recovery codes), /disable, and /recovery-codes. Login enforces the second factor when enabled (auth.mfa_required, then a TOTP or single-use recovery code).
INFO
/v1/auth/sessions and /v1/auth/org/switch are not mounted in the current build.
Example: register
POST /v1/auth/register
Content-Type: application/json
{ "email": "owner@acme.com", "password": "a-strong-password", "orgName": "Acme" }{
"ok": true,
"data": { "userId": "d1f0...", "emailVerificationSent": true },
"meta": { "requestId": "req_01J..." }
}Organizations (/v1/organizations)
Every route requires a user; the service enforces per-org RBAC and tenant scope.
| Method | Path | Min role |
|---|---|---|
| GET | /v1/organizations | viewer (member) |
| POST | /v1/organizations | any user (caller becomes owner) |
| GET | /v1/organizations/:id | member |
| PATCH | /v1/organizations/:id | organization_admin |
| DELETE | /v1/organizations/:id | organization_owner |
| GET | /v1/organizations/:id/members | member |
| POST | /v1/organizations/:id/members | organization_admin |
| PATCH | /v1/organizations/:id/members/:userId | organization_admin |
| DELETE | /v1/organizations/:id/members/:userId | organization_admin |
| POST | /v1/organizations/:id/erase | organization_owner |
| POST | /v1/organizations/:id/api-keys | organization_admin |
| GET | /v1/organizations/:id/api-keys | organization_admin |
| DELETE | /v1/organizations/:id/api-keys/:keyId | organization_admin |
| POST | /v1/organizations/:id/transfer | organization_owner |
| POST | /v1/organizations/:id/transfer/cancel | organization_owner |
| POST | /v1/organizations/transfers/:token/accept | the target user |
POST /v1/organizations/:id/erase is GDPR Article 17 hard erasure: it requires a typed confirmation matching the org name and returns 202.
API keys are created under /v1/organizations/:id/api-keys (the bnq_live_* secret is shown once). Organization ownership transfer is a pending-then-accept flow: the owner initiates to a target email, the recipient accepts with the returned token, and the reassignment runs as the org.transfer.execute job.
Websites (/v1/websites)
Fully implemented.
| Method | Path | Min role |
|---|---|---|
| GET | /v1/organizations/:orgId/websites | viewer |
| POST | /v1/organizations/:orgId/websites | organization_admin |
| GET | /v1/websites/:id | viewer |
| PATCH | /v1/websites/:id | editor |
| DELETE | /v1/websites/:id | organization_admin |
Website transfer and webhook management are available:
| Method | Path | Min role |
|---|---|---|
| POST | /v1/websites/:id/transfer | organization_admin (source) + organization_owner (target) |
| POST | /v1/websites/:id/webhooks | organization_admin |
| GET | /v1/websites/:id/webhooks | organization_admin |
| PATCH | /v1/websites/:id/webhooks/:webhookId | organization_admin |
| DELETE | /v1/websites/:id/webhooks/:webhookId | organization_admin |
A webhook's signing secret is returned once on create and stored only as a hash.
Verification (/v1/websites/:id/verification)
Fully implemented, including the state machine and an SSRF-guarded challenge fetch.
| Method | Path | Min role | Status |
|---|---|---|---|
| GET | /v1/websites/:id/verification | viewer | 200 |
| POST | /v1/websites/:id/verification/challenge | editor | 201 |
| POST | /v1/websites/:id/verification/check | editor | 200 |
challenge issues a token to place via dns_txt, html_meta/meta_tag, well_known_file, or plugin; the method drives the trust tier (see Core concepts). check re-reads ownership and advances the state machine.
Connectors (/v1/websites/:id/connectors)
Fully implemented. The created connector token is shown once at creation; only the authHash and tokenVersion persist.
| Method | Path | Min role | Status |
|---|---|---|---|
| POST | /v1/websites/:id/connectors | organization_admin | 201 (token shown once) |
| GET | /v1/websites/:id/connectors | user | 200 (never returns secrets) |
| POST | /v1/connectors/:cid/rotate | user | 200 (new token shown once) |
| DELETE | /v1/connectors/:cid | user | 204 |
After creation the connector authenticates against the ingestion surface, not these routes.
Example: create connector
{
"ok": true,
"data": {
"id": "9d4e2c10-...",
"name": "wp-prod",
"connectorType": "wordpress",
"token": "9d4e2c10-....base64urlsecret",
"scope": { "websiteId": "a7b2-...", "sourceTypes": ["page", "post", "product"] },
"tokenVersion": 1
},
"meta": { "requestId": "req_01J..." }
}The token is connectorId.secret, the exact value the SDK and plugins sign with. See Ingestion and signing for the scheme.
Knowledge (/v1/websites/:id/...)
Read and search over the knowledge graph, tenant scoped. Fully implemented against the Postgres graph store.
| Method | Path | Min role | Notes |
|---|---|---|---|
| GET | /v1/websites/:id/knowledge/search?q=... | viewer | ranked entity, fact, and chunk hits; q required |
| GET | /v1/websites/:id/entities/:eid/facts | viewer | verified facts for one entity (hidden excluded) |
| GET | /v1/websites/:id/entities/:eid/related | viewer | related entities via graph traversal |
Search and the per-entity reads above are what the knowledge surface exposes today; the broad collection list reads (/facts, /entities, /relationships, /chunks) are not mounted in the current build. The MCP server exposes the same knowledge service for agents.
Publishing and preview (/v1/websites/:id/...)
Drives the exporter registry and the atomic, versioned publish.
| Method | Path | Min role | Behavior |
|---|---|---|---|
| POST | /v1/websites/:id/publish | editor | runs the exporter pipeline synchronously, returns 200 with { versionUlid, versionLabel, files, cdnPath } |
| GET | /v1/websites/:id/node/preview | viewer | the "what AI sees" preview |
| DELETE | /v1/websites/:id/node | organization_admin | takedown: deletes the latest/manifest.json pointer (node serves 404/410), keeps versions for audit; 202 |
Publish runs synchronously and returns 200 (a node.publish worker job also exists and is wired, but this route does the direct engine call). It does not yet pre-gate on website.not_verified. A concurrent publish of the same node holds a per-site Redis lock and returns publish.conflict. See Node files for the published file set and the versions/{ulid}/ plus latest/ layout.
DELETE node has a known bug
DELETE /v1/websites/:id/node is wired, but a known defect in the takedown path can return a server error in the current build. Treat the takedown route as not yet reliable. The published node and its version history are unaffected.
Billing and entitlements (/v1/organizations/:id/...)
Read endpoints are fully implemented; plan, capabilities, and limits are plan-derived constants and usage is read from a repo. Gated by requireOrgScope (the principal's organizationId must equal :id, or admin/super_admin bypasses).
| Method | Path | Min role |
|---|---|---|
| GET | /v1/organizations/:id/entitlements | org member (scoped) |
| GET | /v1/organizations/:id/usage | org member (scoped) |
Billing
The entitlements and usage reads above are live. Live payment processing (Stripe checkout, portal, webhooks) is wired up at launch; in the current build a checkout attempt surfaces billing.unconfigured (503).
Admin (/v1/admin)
Every route requires admin (or super_admin) and every access writes an audit_log row. Fully implemented.
| Method | Path | Min role |
|---|---|---|
| GET | /v1/admin/organizations | admin |
| GET | /v1/admin/organizations/:id/members | admin |
| GET | /v1/admin/audit-log | admin |
| GET | /v1/admin/overview | admin |
| POST | /v1/admin/impersonate | admin |
POST /v1/admin/impersonate mints a real short-lived (900s) access JWT for the target user and writes the audit row. The minted token omits role and org; the real-admin marker lives in the audit log.
Analytics (/v1/websites/:id/analytics)
Read-only per-website rollups. Requires a user access token (any role; light tenant scope).
| Method | Path | Min role |
|---|---|---|
| GET | /v1/websites/:id/analytics | viewer (member) |
{
"ok": true,
"data": {
"websiteId": "b7c8d9e0-...",
"available": true,
"series": [
{ "metric": "node.reads", "value": 1820, "periodStart": "2026-06-01T00:00:00Z" },
{ "metric": "facts.published", "value": 88, "periodStart": "2026-06-01T00:00:00Z" }
]
},
"meta": { "requestId": "req_01J..." }
}Waitlist (/v1/waitlist)
Public marketing capture. No auth; abuse-controlled by the pre-auth rate limiter.
| Method | Path | Auth | Status |
|---|---|---|---|
| POST | /v1/waitlist | none | 202 |
A valid submit always returns 202. A duplicate email returns the same 202, membership is never leaked. A malformed email or missing consent returns 422 validation.failed; abuse returns 429 quota.rate_limited.
POST /v1/waitlist
Content-Type: application/json
{ "email": "future-customer@acme.com", "source": "landing_hero", "consent": true }{ "ok": true, "data": { "status": "accepted" }, "meta": { "requestId": "req_01J..." } }Related
- Ingestion and signing: the connector-facing ingest surface.
- Envelope and errors: the response wrapper and full error catalogue.
- Interactive reference: every endpoint rendered from the spec.
- Node files: what a publish produces.