VisibleBrand.ai API

The REST, WebSocket and SSE surface of the VisibleBrand.ai agentic AI-visibility platform.

OpenAPI 3.1.1api v0.1.0105 endpoints3 realtime channels122 schemas

Generated from the zod contracts in packages/contracts and the NestJS routes in apps/api — never hand-written, and CI fails when the committed artifact differs from a fresh generation.

Scoping. Every workspace-scoped route requires the X-Workspace-Id header. It is asserted before any repository is touched, and Postgres row-level security is the second belt behind it. GET /workspaces is what produces a valid value.

Authorization. Workspace roles form a strict ladder — viewer < approver < owner. Each route documents its MINIMUM role, read directly off the @RequireRole decorator on the handler.

Errors. Every error is application/problem+json (RFC 7807). The type is a stable URI under https://visiblebrand.ai/problems/, so clients should switch on type, not on the human-readable title or detail. Structured evidence (lint violations, policy excerpts, retryAfterSeconds) rides as extension members.

CSRF. State-changing requests that carry the session cookie must also send the X-VB-Csrf header matching the vb_csrf cookie. Callers that authenticate with an explicit header instead of a cookie (the internal surfaces) carry no ambient authority and are therefore not CSRF-gated.

Approvals are the safety boundary. No agent can reach a production write without a human approving a gate. Approval tokens are single-use and bound to a hash of the exact arguments, and they are validated at the Gatekeeper hook AND again at the server layer. No approval token is ever returned over HTTP.

Non-REST boundaries. The live board is a WebSocket channel and the two agent surfaces are SSE streams. All three are documented from the same zod contracts, under the x-vb-channels extension.

Machine-readable spec

The same document this page renders, served as JSON so you can generate a client from it: /docs/openapi.json.

curl -sS https://app.visiblebrand.ai/docs/openapi.json -o openapi.json
npx @openapitools/openapi-generator-cli generate -i openapi.json -g typescript-fetch -o ./client

Authentication & scoping

Four authorities exist and each route documents exactly which of them it accepts. Two are customer-facing, two are internal service secrets and are labelled as such wherever they appear.

sessionCookie cookie vb_session

The signed, session-bound cookie issued by POST /auth/login, POST /auth/signup or the Google OIDC callback. Rotated on every privilege change; the durable sessions row is the liveness authority, so a logout applies across replicas and survives a restart.

csrfToken header X-VB-Csrf

Signed, session-bound double-submit token. Send the value returned by GET /auth/csrf; the matching vb_csrf cookie must accompany it. Required on every state-changing request that carries the session cookie (a bare double-submit is forgeable by anyone who can write a cookie, so the token is bound to the session and a rotated session invalidates its predecessor's token).

ingestToken header x-vb-ingest-token

INTERNAL SERVICE SECRET (VB_INGEST_TOKEN), compared in constant time. Fail-closed ladder: unset on the server means the whole surface is OFF (503 ingest_disabled), a missing or wrong header is 401. Not a customer credential — these routes are the agent-runtime's.

adminToken header x-vb-admin-token

PLATFORM-ADMIN SECRET (VB_ADMIN_TOKEN), compared in constant time. Unset means the surface is OFF (503 admin_disabled), never open. Used by the KYB review queue, which spans workspaces and therefore cannot be workspace-scoped.

X-Workspace-Id header

The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

A first call, end to end

VB_API=https://app.visiblebrand.ai

# 1. sign in — the session cookie and a CSRF pair come back together
curl -sS -c jar.txt "$VB_API/api/v1/auth/csrf" >/dev/null
VB_CSRF=$(grep vb_csrf jar.txt | awk '{print $7}')
curl -sS -b jar.txt -c jar.txt -X POST "$VB_API/api/v1/auth/login" \
  -H "Content-Type: application/json" -H "X-VB-Csrf: $VB_CSRF" \
  -d '{"email":"you@example.com","password":"…"}'

# 2. find the workspace you are a member of
curl -sS -b jar.txt "$VB_API/api/v1/workspaces"

# 3. every scoped call carries the workspace header
curl -sS -b jar.txt -H "X-Workspace-Id: $VB_WORKSPACE" \
  "$VB_API/api/v1/metrics/visibility?engine=chatgpt"

Authorization

Workspace roles are a strict ladder: viewer < approver < owner. A route requiring `approver` therefore admits approvers and owners.

The role on each endpoint below is read off the @RequireRole decorator on the handler that serves it, so this reference cannot describe a permission the server does not enforce. Two policies sit deliberately outside the ladder and are worth knowing before you build against the write path:

  • Approval is the only path to a production write. No role, no plan and no autonomy setting lets an agent write to a customer’s site or publish content without a human approving a gate. The approval token is single-use and bound to a hash of the exact arguments, it is validated at the agent hook and again server-side, and it is never returned over HTTP.
  • Trust tier is enforced at token mint. A workspace below the required tier gets 403 tier_requirement and the gate stays pending — it is not silently downgraded, and the check is not duplicated (and therefore cannot disagree with itself) anywhere else.
  • Entitlements fail closed. Trial and manual-assist plans are refused the write path with 403 entitlement_writes_refused before any pipeline starts, so no gate and no staged change is created.

Errors

Every error is RFC 7807 application/problem+json. The type is a stable URI; title and detail are for humans and may be reworded at any time, so client logic should switch on type (or the machine code at the end of it) and nothing else.

{
  "type": "https://visiblebrand.ai/problems/tier-requirement",
  "title": "Tier requirement",
  "status": 403,
  "detail": "workspace trust tier 't1' is below 't2' required to approve a placement purchase"
}

Structured evidence rides as extension members rather than being flattened into the detail string: prompt-bank lint violations, content-policy violations with their exact draft excerpts, and retryAfterSeconds on a throttled credential endpoint (which also sets Retry-After).

The full code list is in the problem code reference 72 codes, read out of the api’s own registry.

Realtime channels

The board is live over WebSocket and both agent surfaces stream over SSE. All three are documented from the same zod contracts as the REST surface — a reference that stopped at REST would leave out how the product actually behaves once it is open.

Board channel

websocket/ws?workspace={workspaceId}

The live workspace board. The server pushes every board event with its log-assigned `event_id`; clients send optimistic operations carrying a `client_op_id` and receive an `ack` frame echoing it, which is what lets the UI reconcile an optimistic move against the authoritative result. After a disconnect the client sends `{resume_from: <last event_id>}` and receives the gap, then live frames — replay is de-duplicated by `event_id`, so a reconnect is never visible as duplicated work (AC-06-02).

Authentication

PER-CONNECTION, exactly like a REST request: the vb_session cookie travels on the upgrade, its signature is verified, the durable sessions row is the liveness authority, and the caller must hold a membership in the requested workspace. Holding a workspace uuid alone subscribes to nothing. Unknown-workspace and not-a-member answer the SAME code (`workspace_access_denied`) so the channel cannot be used to probe which workspaces exist. Mutating frames re-check >= approver PER OPERATION, so a mid-connection role revocation stops writes on the next frame.

Frames

server → client Board event · WsServerEvent

A discriminated union on `t`: ticket.created, ticket.updated, comment.created, agent.status, gate.created, gate.resolved, blocker.raised, standup.posted, connector.suggested, presence. Every event carries `event_id`, and the board state is reconstructable from the event log alone (AC-04-04).

{
  "t": "ticket.updated",
  "event_id": "1723891200000-3",
  "ticket": {
    "id": "9f1c0b6e-3a2d-4e4f-9c1b-2a5d7e8f0011",
    "boardColumn": "needs_approval",
    "working": false,
    "blocked": false
  }
}

WsServerEvent

One of 10 variants:

ticket.updated
FieldTypeNotes
t req"ticket.updated"
event_id reqstringmin length 1
ticket reqTicketSummary
ticket.created
FieldTypeNotes
t req"ticket.created"
event_id reqstringmin length 1
ticket reqTicketSummary
comment.created
FieldTypeNotes
t req"comment.created"
event_id reqstringmin length 1
ticket_id reqstring(uuid)
comment reqComment
agent.status
FieldTypeNotes
t req"agent.status"
event_id reqstringmin length 1
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
state req"idle" | "working" | "blocked"
ticket_idstring(uuid)
gate.created
FieldTypeNotes
t req"gate.created"
event_id reqstringmin length 1
gate reqGate
gate.resolved
FieldTypeNotes
t req"gate.resolved"
event_id reqstringmin length 1
gate_id reqstring(uuid)
status reqenum(5)pending, approved, rejected, expired, auto_approved
blocker.raised
FieldTypeNotes
t req"blocker.raised"
event_id reqstringmin length 1
ticket_id reqstring(uuid)
blocker reqobject
standup.posted
FieldTypeNotes
t req"standup.posted"
event_id reqstringmin length 1
comment_id reqstring(uuid)
connector.suggested
FieldTypeNotes
t req"connector.suggested"
event_id reqstringmin length 1
workspaceId reqstring(uuid)
provider reqenum(7)wordpress, shopify, webflow, wix, squarespace, nextjs, unknown
confidence req"confirmed" | "likely"
evidence reqobject[]max 20 items
message reqBilingualText
occurredAt reqstring(date-time)
presence
FieldTypeNotes
t req"presence"
event_id reqstringmin length 1
users reqobject[]
server → client Operation ack · WsAckFrame

Echoes the `client_op_id` of a client operation with `ok` plus, on failure, the use-case error code and message. This is the frame an optimistic UI reconciles against — a rejected move is rolled back in the client rather than left looking applied.

{
  "t": "ack",
  "client_op_id": "op-7",
  "ok": false,
  "error": "invalid_transition",
  "message": "in_review -> backlog is not a legal move"
}

WsAckFrame

FieldTypeNotes
t req"ack"
client_op_id reqstringmin length 1
ok reqboolean
errorstringmin length 1
messagestring
server → client Protocol error · WsErrorFrame

A protocol- or authorization-level fault: invalid_json, invalid_frame, workspace_query_invalid, unauthenticated, workspace_access_denied. The connection is closed with 1008 after an authorization fault.

{
  "t": "error",
  "code": "workspace_access_denied"
}

WsErrorFrame

FieldTypeNotes
t req"error"
code reqenum(6)invalid_json, invalid_frame, workspace_query_invalid, workspace_not_found, unauthenticated, workspace_access_denied
message reqstring
client → server Client operation · WsClientMessage

ticket.move, comment.create or typing — each with a `client_op_id` the server echoes in its ack. Board writes over WebSocket enforce the same RBAC ladder as their REST twins.

{
  "t": "ticket.move",
  "client_op_id": "op-7",
  "ticket_id": "9f1c0b6e-3a2d-4e4f-9c1b-2a5d7e8f0011",
  "to": "in_review"
}

WsClientMessage

One of 3 variants:

ticket.move
FieldTypeNotes
t req"ticket.move"
client_op_id reqstringmin length 1
ticket_id reqstring(uuid)
to reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled

Unknown properties are stripped rather than rejected.

comment.create
FieldTypeNotes
t req"comment.create"
client_op_id reqstringmin length 1
ticket_id reqstring(uuid)
comment reqCommentCreate

Unknown properties are stripped rather than rejected.

typing
FieldTypeNotes
t req"typing"
client_op_id reqstringmin length 1
ticket_id reqstring(uuid)

Unknown properties are stripped rather than rejected.

client → server Resume request · WsResumeRequest

Sent immediately after reconnecting. The server replays every event after `resume_from`, then follows live.

{
  "resume_from": "1723891200000-3"
}

WsResumeRequest

FieldTypeNotes
resume_from reqstringmin length 1

Unknown properties are stripped rather than rejected.

Notes

  • The channel is mounted on the same HTTP server as the REST API but is NOT under /api/v1 — connect to /ws directly.
  • Heartbeats every 15s; a peer that misses a full cycle is dropped.
  • The board is fan-out, never the record: the durable event log is the authority a client resumes against.

Ticket activity stream

sse/api/v1/tickets/{id}/stream

What an agent is doing on one ticket, as Server-Sent Events: session start, curated thought summaries, tool calls and results, text deltas, artifacts, gate requests and the session end. Replays the recent ring, then follows live. The `id:` line carries the ring seq.

Authentication

The same session cookie and workspace header as any REST read (>= viewer). Workspace scope is asserted BEFORE any stream bytes are written, so a cross-workspace ticket id never opens a stream.

Frames

server → client Ticket stream event · TicketStreamEvent

A discriminated union on `t`. `thought.summary` is CURATED progress, never raw chain-of-thought (DECISION-06-02) — only curated frames ever enter the buffer, so there is no configuration in which a client can read the model's unfiltered reasoning.

{
  "t": "tool.call",
  "tool": "cms.stage_change",
  "summary": "Staging robots.txt update"
}

TicketStreamEvent

One of 9 variants:

session.started
FieldTypeNotes
t req"session.started"
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
thought.summary
FieldTypeNotes
t req"thought.summary"
text reqstring
tool.call
FieldTypeNotes
t req"tool.call"
name reqstringmin length 1
summary reqstring
activityBilingualText
tool.result
FieldTypeNotes
t req"tool.result"
name reqstringmin length 1
summary reqstring
activityBilingualText
text.delta
FieldTypeNotes
t req"text.delta"
text reqstring
message_seqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
message_offsetinteger>= 0 · <= 9007199254740991
message.closed
FieldTypeNotes
t req"message.closed"
message_seq reqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
artifact.created
FieldTypeNotes
t req"artifact.created"
kind reqstringmin length 1
path reqstringmin length 1
preview_urlstring(uri)
gate.requested
FieldTypeNotes
t req"gate.requested"
gate_id reqstring(uuid)
session.ended
FieldTypeNotes
t req"session.ended"
result req"done" | "blocked" | "failed"

Notes

  • Heartbeat comment (`: hb`) every 15s. `X-Accel-Buffering: no` is set so proxies do not buffer frames.
  • EventSource cannot set headers; browser clients rely on the cookie and the same-origin proxy for X-Workspace-Id.

Chat stream

sse/api/v1/chat/{chatSessionId}/stream

The agent conversation, live. Replays the DURABLE transcript from the resume cursor and only then follows the in-process ring — the ring is fan-out, never the record. Replay-then-live is gap-free: a buffering listener attaches synchronously, so frames produced while the transcript read is in flight are captured and then de-duplicated by durable seq.

Authentication

Session cookie + X-Workspace-Id, >= viewer. Unknown and cross-workspace sessions answer the identical 404.

Frames

server → client Chat stream event · ChatStreamEvent

Text deltas, tool lines and message cards (the editable profile-draft card, credential requests, plan proposals). Human turns have NO frame in the grammar and none is invented — dressing a human turn as agent output would put words in the agent's mouth — so they advance the resume cursor with an `id:` line and a comment naming where to read them.

{
  "t": "text.delta",
  "text": "I found your site and read the homepage. "
}

ChatStreamEvent

One of 10 variants:

session.started
FieldTypeNotes
t req"session.started"
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
thought.summary
FieldTypeNotes
t req"thought.summary"
text reqstring
tool.call
FieldTypeNotes
t req"tool.call"
name reqstringmin length 1
summary reqstring
activityBilingualText
tool.result
FieldTypeNotes
t req"tool.result"
name reqstringmin length 1
summary reqstring
activityBilingualText
text.delta
FieldTypeNotes
t req"text.delta"
text reqstring
message_seqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
message_offsetinteger>= 0 · <= 9007199254740991
message.closed
FieldTypeNotes
t req"message.closed"
message_seq reqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
artifact.created
FieldTypeNotes
t req"artifact.created"
kind reqstringmin length 1
path reqstringmin length 1
preview_urlstring(uri)
gate.requested
FieldTypeNotes
t req"gate.requested"
gate_id reqstring(uuid)
session.ended
FieldTypeNotes
t req"session.ended"
result req"done" | "blocked" | "failed"
message.card
FieldTypeNotes
t req"message.card"
card reqobject | object | object

Notes

  • `id:` carries the durable chat_messages.seq. Frames with no durable row carry NO `id:`, so Last-Event-ID never advances past something the transcript could not replay.
  • Resume with the browser's automatic Last-Event-ID header, or explicitly with ?afterSeq=. The header wins — it is what the client actually received.

Stability & versioning

Where the version comes from

info.version is 0.1.0, taken from apps/api/package.json — the api’s own version, the same value GET /health advertises. Generation fails if those two ever disagree, so a running server and the document describing it always claim the same number. There is deliberately no separate “spec version” to keep in step, and no repo version (the workspace root is private and unversioned).

The /api/v1 path prefix is the wire version and moves only for a breaking change. info.version moves with the api release.

How a breaking wire change is made

Every boundary schema lives in packages/contracts, and the binding architecture decisions that govern them (DECISION-xx-nn) change only through an ADR in docs/adr/. So a breaking wire change is a four-step act, in this order:

  1. Write the ADR. It records what breaks, who is affected, and the migration. A contracts change without one is a review failure, not a merge conflict to resolve later.
  2. Expand, then migrate, then contract — the same discipline the database migrations use. Add the new field as optional, ship clients onto it, and only then remove the old one. A field going from optional to required, or disappearing, is a break even when no test notices.
  3. Regenerate. pnpm openapi:generate, and commit docs/api/openapi.json in the same pull request. This is what makes the wire change visible in the diff to a reviewer.
  4. Bump the prefix only if it truly breaks. A removal or a semantic change of an existing field needs /api/v2 alongside /api/v1; additive change does not.

Additive and therefore safe: a new optional request field, a new response field, a new enum member on a value the server produces, a new endpoint. Breaking: removing or renaming anything, tightening validation, adding a required request field, adding an enum member to a value the server accepts (old clients will not know to handle the new one coming back), or changing a status code.

Why this page cannot go stale

The document is generated, never written: the route table is reflected off the NestJS controllers and the schemas are the zod objects the server validates with. CI runs pnpm openapi:check, which regenerates and fails when the committed artifact differs, and a reconciliation test asserts in both directions that every registered route is documented and every documented route exists. Adding a controller route without documenting it breaks the build.

apps/api/src/docs/generate.ts + packages/contracts/src/openapi — regenerate with `pnpm openapi:generate`; CI fails when the committed artifact differs (`pnpm openapi:check`).

One honest gap

38 of the 122 schemas below are marked mirror. Those shapes are declared inside apps/api — controller-local request schemas and use-case DTOs that have not been promoted into packages/contracts yet — so they are described here rather than derived, and they are the only part of this document that a code change could contradict without CI noticing. Each one names the file it mirrors. Promoting them into contracts is what closes the gap; the endpoints themselves cannot drift either way.

Problem code reference

Every code the api can answer with, read out of its own registry. The type URI is the stable identifier.

CodeStatusMeaning
admin_disabled503Platform-admin surface (x-vb-admin-token; ingest-token guard precedent): VB_ADMIN_TOKEN unset — the admin surface is off, never open
admin_token_invalid401header missing or mismatched — fail closed
agent_not_available501addon grants the seat but the agent has no v1 roster row (v1.5/v2 keys)
args_hash_mismatch403approve change A, attempt payload B (DECISION-04-06)
artifact_path_outside_root400Market-brief artifact loading (spec 02 §2 surface): the stored content_path must resolve UNDER the configured artifacts root (VB_ARTIFACTS_ROOT) — a row pointing outside it is refused, never read.
bank_not_found404Bank not found
brief_not_found404market-brief surface: no brief versions yet / unknown version
change_not_found404spec 05 §6 site_changes lookup by row id / change_id
chat_event_invalid422internal ingest: api re-validates each event against ChatStreamEvent
chat_session_not_found404spec 06 §7 chat channel: unknown session or wrong workspace
csrf_token_invalid403Csrf token invalid
csrf_token_missing403CSRF (spec 13 §3) — auth/csrf.guard.ts.
destination_unsupported422only client_site has a v0 connector publish path
draft_not_found404unknown content draft id / wrong workspace
email_taken409Signup on an address that already exists (@vb/app RegisterUser). 409, not 422: the body is well-formed and semantically valid, it conflicts with existing server state (RFC 9110 §15.5.10). The disclosure is deliberate and bounded — the honest alternative (pretend success) either strands the caller or mails a stranger, the detail never says whether the existing account signs in with a password or with Google, and the route carries the same per-email+IP / per-IP throttle as login so it cannot be driven as a bulk address oracle.
empty_bank422Empty bank
entitlement_writes_refused403Billing & entitlements (E10, spec 15; AC-15-02 server-side enforcement): trial/manual-assist plans never execute production writes (DECISION-15-02)
execution_failed502connector crashed mid-write (rollback token durable)
execution_refused403Site-change pipeline (E07, spec 02 §4 / spec 04 §6): token forged/expired/replayed/mis-bound — AC-04-02 posture
frozen_bank409spec 06 §4: prompt edits target draft banks only
gate_not_approved409manual retry requires a human-approved gate first
gate_not_found404Gate not found
ingest_disabled503VB_INGEST_TOKEN unset — the ingest surface is off, never open
ingest_token_invalid401Internal event ingest (packages/contracts ingest.ts; spec 04 §5 dev transport): x-vb-ingest-token missing or mismatched — fail closed
integration_not_connected409refresh needs a connected row with a vault ref
integration_not_found404spec 06 §5: unknown integration id / wrong workspace
invalid_credentials401Password auth (spec 11 §2). ONE code for "unknown email" and "wrong password" on purpose — a distinct code would turn the login endpoint into a user-enumeration oracle (see auth/auth.controller.ts).
invalid_transition409Invalid transition
invariant_violation422Invariant violation
kyb_requires_t1409T2 brand verification — manual KYB queue (spec 23 §1): brand verification stacks on a domain-verified workspace (T2 = T1 + KYB)
no_frozen_bank409In-product run triggers (PM audit #5 — orchestration/run-triggers): spec 03 §3: measurement runs against a FROZEN bank only — freeze first
no_pending_kyb409decide targets a workspace with no pending KYB request
no_pending_verification409spec 23 §1: POST /verification/check with no pending request — start first.
not_a_member403spec 05 §1: unknown workspace membership fails closed
not_found404Not found
not_implemented501Not implemented
notification_not_found404spec 26 §2 inbox: unknown id or another user's row
oauth_callback_invalid400Google OIDC callback taxonomy (spec 11 §2 OAuth primary, spec 13 §3): Google returned error=... or code/state missing
oauth_email_unverified403accounts bind by email; unverified is refused
oauth_exchange_failed502Google token endpoint rejected the exchange
oauth_id_token_invalid401signature/iss/aud/exp/nonce check failed
oauth_refresh_invalid_grant409GSC/GA4 consent + token-refresh taxonomy (spec 07 §7): refresh token expired/revoked per Google docs — re-auth required
oauth_state_mismatch400unknown/expired/replayed state — CSRF posture
oauth_upstream_unavailable502discovery/JWKS fetch failed or malformed
placement_kind_unavailable422the property does not offer this placement kind (null price)
placement_not_found404unknown placement id / wrong workspace
policy_failed422Content publish pipeline (E09, spec 02 §5 / spec 24): DECISION-24-01: draft failed policy; violations ride the body
policy_pass_required409gated draft without a passing stored verdict (corrupt state)
profile_approved_immutable409IMPLEMENTATION DECISION (spec 06 §1 is silent past "draft edits"): PATCH /profile on an APPROVED profile conflicts with the domain rule "approved exactly once, then read-only" (spec 05 §2) — 409, never a silent flip back to draft; re-onboarding creates a new draft instead.
profile_not_found404Profile not found
prompt_bank_lint_failed422AC-02-03: brand leaks block promptbank.approved
prompt_not_found404Prompt not found
property_not_found404Placements (E11, spec 06 §6, spec 02 §6): unknown/inactive network property key
provider_not_connectable501spec 06 §5 connect: provider exists in the enum but has no v1 connect flow yet (spec 07 §2-§4 are v1.5/v2) — honest 501, never an invented flow.
provider_verification_failed422spec 06 §5 credentials: the vendor itself refused the submitted credentials during the pre-store verification probe (integrations/provider-probe.ts). 422 — the request is well-formed, the values do not work against the vendor.
queue_unavailable503REDIS_URL unset or enqueue failed — nothing would pick the job up
rate_limited429Credential-endpoint throttling (spec 11 §2); carries Retry-After.
refresh_not_supported400token refresh applies to the Google OAuth providers only
role_requirement403spec 05 §1 role ladder viewer < approver < owner
rollback_failed502compensating rollback errored; manual retry remains
run_not_found404spec 06 §4 GET /runs/{id}/samples: unknown run or wrong workspace
run_trigger_requires_database501in-memory api mode: the fleet pipeline writes to Postgres
staging_failed502connector could not stage the change
ticket_not_found404Ticket not found
tier_requirement403AC-23-01: under-tier approvals are refused, gate stays pending
unauthorized401Unauthorized
validation_failed400Validation failed
verification_failed502post-write verify failed; production auto-rolled back
workspace_domain_missing422POST /audits: the workspace has no primary_domain to audit
workspace_header_invalid400Workspace header invalid
workspace_header_missing400Workspace header missing
workspace_not_found404Workspace not found

Identity & sessions

Sign-up, password login, Google OIDC, the session cookie and the CSRF token pair. Not workspace-scoped — identity precedes scope.

GET/auth/csrf#

Issue a CSRF token pair

Sets the vb_csrf cookie and returns the matching token to send as X-VB-Csrf. The token is BOUND to the current session, so it must be re-fetched after login or logout (both of those responses already ship a replacement cookie). GET /auth/me also refreshes a stale pair.

Safe method, so the CSRF guard does not gate it — this is the bootstrap every state-changing call depends on.

no workspace rolenot workspace-scopedunauthenticatedAuthController.csrf

Request

curl -sS -X GET "$VB_API/api/v1/auth/csrf"

Response 200 OK

A fresh token plus the header and cookie names to use.

CsrfTokenResponse

FieldTypeNotes
token reqstring
header reqstring
cookie reqstring
Errors — 0 problem codes across 1 status
StatusCodetype

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/auth/dev-login#

Dev-only find-or-create login

Convenience entry for local development: finds or creates a user by email and sets the session cookie. HARD-DISABLED when NODE_ENV=production, where it answers 404 — password login and Google OIDC are the only production entries.

Never available in production.

no workspace rolenot workspace-scopedCSRF token requiredheader X-VB-CsrfAuthController.devLogin

Request

curl -sS -X POST "$VB_API/api/v1/auth/dev-login" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

DevLoginRequest

FieldTypeNotes
email reqstring(email)

Unknown properties are stripped rather than rejected.

Response 200 OK

Signed in as the named address.

AuthIdentity

FieldTypeNotes
userId reqstring(uuid)
email reqstring
name reqstring | null
locale req"ar" | "en"
issuedAt reqstring(date-time)
expiresAt reqstring(date-time)
memberships reqMembershipSummary[]
Errors — 4 problem codes across 4 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
404not_foundhttps://visiblebrand.ai/problems/not-found
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/auth/google#

Begin Google OIDC

302 to the Google consent screen with PKCE S256 and a bound single-use state. Answers 501 problem+json while GOOGLE_CLIENT_ID/SECRET are unset — the stub adapter throws before any redirect happens.

no workspace rolenot workspace-scopedunauthenticatedAuthController.googleStart

Request

curl -sS -X GET "$VB_API/api/v1/auth/google"

Response 200 OK

302 to accounts.google.com.

Errors — 1 problem code across 2 statuses
StatusCodetype
501not_implementedhttps://visiblebrand.ai/problems/not-implemented

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/auth/google/callback#

Google OIDC callback

Validates state, PKCE and the id_token (local JWKS RS256 verification), maps the verified email to a user and issues the standard session cookie — OAuth is a way to OBTAIN a session, not a parallel auth system. Then 302 to the web app.

No CSRF token and none needed: it is a GET, and its anti-forgery control is the bound single-use state parameter.

no workspace rolenot workspace-scopedunauthenticatedAuthController.googleCallback

Request

curl -sS -X GET "$VB_API/api/v1/auth/google/callback"

Parameters

NameInTypeDescription
codequerystringGoogle authorization code.
statequerystringThe bound single-use state issued at /auth/google.
errorquerystringPresent when the user denied consent.

Response 200 OK

302 back to the web app.

Errors — 7 problem codes across 5 statuses
StatusCodetype
400oauth_callback_invalidhttps://visiblebrand.ai/problems/oauth-callback-invalid
403oauth_email_unverifiedhttps://visiblebrand.ai/problems/oauth-email-unverified
502oauth_exchange_failedhttps://visiblebrand.ai/problems/oauth-exchange-failed
401oauth_id_token_invalidhttps://visiblebrand.ai/problems/oauth-id-token-invalid
400oauth_state_mismatchhttps://visiblebrand.ai/problems/oauth-state-mismatch
502oauth_upstream_unavailablehttps://visiblebrand.ai/problems/oauth-upstream-unavailable
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/auth/login#

Password login

An unknown address, an address with no password (SSO-only account) and a wrong password all answer the same 401 invalid_credentials and spend the same Argon2id cost getting there, so the endpoint cannot be timed to enumerate accounts. The throttle is checked BEFORE the hasher.

CSRF required — login-CSRF (silently signing a victim into an attacker-controlled account) is a real attack.

no workspace rolenot workspace-scopedCSRF token requiredheader X-VB-CsrfAuthController.login

Request

curl -sS -X POST "$VB_API/api/v1/auth/login" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

LoginRequest

FieldTypeNotes
email reqstring(email)
password reqstringmin length 1

Unknown properties are stripped rather than rejected.

Response 200 OK

Signed in; the session cookie is set (rotated, not reused).

AuthIdentity

FieldTypeNotes
userId reqstring(uuid)
email reqstring
name reqstring | null
locale req"ar" | "en"
issuedAt reqstring(date-time)
expiresAt reqstring(date-time)
memberships reqMembershipSummary[]
Errors — 5 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
401invalid_credentialshttps://visiblebrand.ai/problems/invalid-credentials
429rate_limitedhttps://visiblebrand.ai/problems/rate-limited
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/auth/logout#

End the session

Revokes the durable session row (so the logout survives a restart and applies to every replica) and clears both the session and CSRF cookies. Idempotent: unknown, expired and already-revoked handles all succeed.

no workspace rolenot workspace-scopedunauthenticatedAuthController.logout

Request

curl -sS -X POST "$VB_API/api/v1/auth/logout"

Response 204 No Content

Session revoked; both cookies cleared.

Errors — 0 problem codes across 1 status
StatusCodetype

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/auth/me#

The signed-in principal

Returns the user, the live session and the caller's workspace memberships. Also re-issues the CSRF cookie so a stale pair self-heals on page load.

no workspace rolenot workspace-scopedunauthenticatedAuthController.me

Request

curl -sS -X GET "$VB_API/api/v1/auth/me"

Response 200 OK

The live principal.

AuthIdentity

FieldTypeNotes
userId reqstring(uuid)
email reqstring
name reqstring | null
locale req"ar" | "en"
issuedAt reqstring(date-time)
expiresAt reqstring(date-time)
memberships reqMembershipSummary[]
Errors — 1 problem code across 2 statuses
StatusCodetype
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/auth/signup#

Create an account

Registers the address, starts a session and returns the principal. Shares the login throttle buckets: without that, this route would be a fast "does this address exist" oracle.

CSRF IS required here even though the caller has no session yet: signup creates ambient authority, and a forged signup plants an attacker-controlled account in the victim's browser.

no workspace rolenot workspace-scopedCSRF token requiredheader X-VB-CsrfAuthController.signup

Request

curl -sS -X POST "$VB_API/api/v1/auth/signup" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

SignupRequest

FieldTypeNotes
email reqstring(email)
password reqstringmin length 12
namestringmin length 1
locale"ar" | "en"

Unknown properties are stripped rather than rejected.

Response 201 Created

Account created; the session cookie is set.

AuthIdentity

FieldTypeNotes
userId reqstring(uuid)
email reqstring
name reqstring | null
locale req"ar" | "en"
issuedAt reqstring(date-time)
expiresAt reqstring(date-time)
memberships reqMembershipSummary[]
Errors — 5 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409email_takenhttps://visiblebrand.ai/problems/email-taken
429rate_limitedhttps://visiblebrand.ai/problems/rate-limited
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

Workspaces & profile

Workspace creation and membership listing (keyed on the authenticated actor), plus the business profile a human approves once.

GET/profile#

The business profile

The profile the onboarding agent drafts and a human approves. Carries the workspace's primaryDomain as an additive read-only field.

role >= viewerX-Workspace-Id requiredcookie vb_sessionProfileController.getProfile

Request

curl -sS -X GET "$VB_API/api/v1/profile" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The current draft or approved profile.

BusinessProfile

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
name reqstringmin length 1
primaryDomainstringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offerings reqobject[]
serviceAreas reqobject[]
languages reqstring[]
personas reqobject[]
socials reqobject[]
brandAliases reqstring[]
competitors reqobject[]default []
techStackobject
fieldProvenancemap
status req"draft" | "approved"
approvedBystring(uuid)
approvedAtstring(date-time)
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404profile_not_foundhttps://visiblebrand.ai/problems/profile-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

PATCH/profile#

Edit the profile draft

Partial DRAFT edits — absent fields stay untouched (competitors never implicitly clear). An APPROVED profile answers 409 profile_approved_immutable: approved once, then read-only. Re-onboarding creates a new draft instead. `primaryDomain` is stripped from the request.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfProfileController.patchProfile

Request

curl -sS -X PATCH "$VB_API/api/v1/profile" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

PatchProfileRequest

FieldTypeNotes
namestringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offeringsobject[]
serviceAreasobject[]
languagesstring[]
personasobject[]
socialsobject[]
brandAliasesstring[]
competitorsobject[]
techStackobject
fieldProvenancemap

Unknown properties are stripped rather than rejected.

Response 200 OK

The updated draft.

BusinessProfile

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
name reqstringmin length 1
primaryDomainstringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offerings reqobject[]
serviceAreas reqobject[]
languages reqstring[]
personas reqobject[]
socials reqobject[]
brandAliases reqstring[]
competitors reqobject[]default []
techStackobject
fieldProvenancemap
status req"draft" | "approved"
approvedBystring(uuid)
approvedAtstring(date-time)
Errors — 12 problem codes across 7 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
409profile_approved_immutablehttps://visiblebrand.ai/problems/profile-approved-immutable
404profile_not_foundhttps://visiblebrand.ai/problems/profile-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/profile/approve#

Approve the profile

Resolves the profile.approved gate. The only path from draft to approved is a human calling this. `activation` reports what the approval STARTED, step by step: the draft question bank (with its id, version and prompt count), the first site audit and the first measurement — each with a real status, and for anything that could not start the named precondition in `waitingOn`. When no bank could be drafted, `missing` names the profile inputs responsible (`offerings`, `category`, `brand_free_terms`) instead of leaving the customer with silence. Absent on an already-approved replay: nothing was re-run.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfProfileController.approveProfile

Request

curl -sS -X POST "$VB_API/api/v1/profile/approve" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The approved profile id, status and what the approval kicked off.

ProfileApproveResponse

FieldTypeNotes
profileId reqstring(uuid)
status req"draft" | "approved"
activationobject
Errors — 10 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404profile_not_foundhttps://visiblebrand.ai/problems/profile-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/workspaces#

The caller's workspaces

Answers with the authenticated actor's memberships and nothing else — there is no global workspace listing on this surface. This is the route that PRODUCES the scope the X-Workspace-Id header carries everywhere else.

no workspace rolenot workspace-scopedcookie vb_sessionWorkspacesController.listWorkspaces

Request

curl -sS -X GET "$VB_API/api/v1/workspaces" \
  -b "vb_session=$VB_SESSION"

Response 200 OK

Every workspace the caller is a member of, with their role.

Errors — 1 problem code across 2 statuses
StatusCodetype
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/workspaces#

Create a workspace

The creator becomes the owner. Bilingual display names round-trip; locale_default is honored.

no workspace rolenot workspace-scopedCSRF token requiredcookie vb_sessionheader X-VB-CsrfWorkspacesController.createWorkspace

Request

curl -sS -X POST "$VB_API/api/v1/workspaces" \
  -b "vb_session=$VB_SESSION" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

CreateWorkspaceRequest

FieldTypeNotes
name reqBilingualTextInput
locale req"ar" | "en"
primaryDomainstring

Unknown properties are stripped rather than rejected.

Response 201 Created

The new workspace and the creator's owner membership.

MembershipSummary

FieldTypeNotes
workspaceId reqstring(uuid)
name reqBilingualText
role req"owner" | "approver" | "viewer"
trustTier req"t0" | "t1" | "t2"
Errors — 5 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

Workspace board

The Kanban surface: tickets, legal column transitions and the comment thread agents and humans share.

GET/board#

The board

All eight columns, empty ones included, each with its ticket summaries.

role >= viewerX-Workspace-Id requiredcookie vb_sessionBoardController.getBoard

Request

curl -sS -X GET "$VB_API/api/v1/board" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The full board for the scoped workspace.

BoardResponse

FieldTypeNotes
columns reqobject[]min 8 items · max 8 items
Example response
{
  "columns": [
    {
      "key": "backlog",
      "tickets": []
    },
    {
      "key": "in_progress",
      "tickets": [
        {
          "id": "9f1c0b6e-3a2d-4e4f-9c1b-2a5d7e8f0011",
          "workspaceId": "3f9a2c10-5b7d-4e21-9a44-8c1e6b0d7f22",
          "boardColumn": "in_progress",
          "title": {
            "en": "Fix robots.txt AI-bot block",
            "ar": "إصلاح حجب روبوتات الذكاء الاصطناعي"
          },
          "taskType": "technical_fix",
          "assignee": {
            "kind": "agent",
            "agentKey": "technical"
          },
          "priority": "p1",
          "commentCount": 3,
          "working": true,
          "blocked": false
        }
      ]
    }
  ]
}
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/tickets#

Create a ticket

Human-created work. Agents create tickets through the orchestration pipeline, not this route.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfBoardController.createTicket

Request

curl -sS -X POST "$VB_API/api/v1/tickets" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

CreateTicketRequest

FieldTypeNotes
title reqstringmin length 1
descriptionMdstring
taskType reqstringmin length 1
assignee reqobject | object
deadlinestring(date)
priority"p0" | "p1" | "p2" | "p3"

Unknown properties are stripped rather than rejected.

Response 201 Created

The created ticket.

TicketSummary

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
boardColumn reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
title reqstringmin length 1
taskType reqstringmin length 1
assignee reqobject | object
priority req"p0" | "p1" | "p2" | "p3"
deadlinestring(date)
commentCount reqinteger>= 0 · <= 9007199254740991
working reqboolean
blocked reqboolean
Errors — 10 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/tickets/{id}#

Ticket detail

The full ticket including its markdown description, payload, gate link and Temporal/session handles.

role >= viewerX-Workspace-Id requiredcookie vb_sessionBoardController.getTicket

Request

curl -sS -X GET "$VB_API/api/v1/tickets/{id}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The ticket.

TicketDetail

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
boardColumn reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
title reqstringmin length 1
taskType reqstringmin length 1
assignee reqobject | object
priority req"p0" | "p1" | "p2" | "p3"
deadlinestring(date)
commentCount reqinteger>= 0 · <= 9007199254740991
working reqboolean
blocked reqboolean
descriptionMd reqstring
payload reqmap
gateIdstring(uuid)
temporalWorkflowIdstring
sessionIdstring
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

PATCH/tickets/{id}#

Move or edit a ticket

Column moves go through the domain entity, so an illegal transition is a 409 invalid_transition rather than a silent write. Only the transitions a human is allowed to make are accepted.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfBoardController.patchTicket

Request

curl -sS -X PATCH "$VB_API/api/v1/tickets/{id}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

PatchTicketRequest

FieldTypeNotes
boardColumnenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
assigneeobject | object
deadlinestring(date) | null
priority"p0" | "p1" | "p2" | "p3"

Unknown properties are stripped rather than rejected.

Response 200 OK

The updated ticket.

TicketSummary

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
boardColumn reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
title reqstringmin length 1
taskType reqstringmin length 1
assignee reqobject | object
priority req"p0" | "p1" | "p2" | "p3"
deadlinestring(date)
commentCount reqinteger>= 0 · <= 9007199254740991
working reqboolean
blocked reqboolean
Errors — 11 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/tickets/{id}/comments#

Comment thread

Newest first, keyset paged. Agent and human comments share one thread; @mentions ride the body.

role >= viewerX-Workspace-Id requiredcookie vb_sessionBoardController.listComments

Request

curl -sS -X GET "$VB_API/api/v1/tickets/{id}/comments" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
cursorquerystring(uuid)Keyset cursor: the id of the last comment from the previous page. Absent starts at the newest row.
limitqueryintegerPage size, 1–100. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of comments (JSON array).

Comment

FieldTypeNotes
id reqstring(uuid)
ticketId reqstring(uuid)
authorKind req"agent" | "human" | "system"
author reqstringmin length 1
bodyMd reqstring
kind reqenum(6)comment, progress, blocker, handoff, standup, system
mentions reqobject[]
blockerobject
createdAt reqstring(date-time)
Errors — 8 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/tickets/{id}/comments#

Post a comment

Appends to the shared thread and emits a comment.created event to the board channel.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfBoardController.createComment

Request

curl -sS -X POST "$VB_API/api/v1/tickets/{id}/comments" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

CommentCreate

FieldTypeNotes
bodyMd reqstringmin length 1
mentionsobject[]

Unknown properties are stripped rather than rejected.

Response 201 Created

The created comment.

Comment

FieldTypeNotes
id reqstring(uuid)
ticketId reqstring(uuid)
authorKind req"agent" | "human" | "system"
author reqstringmin length 1
bodyMd reqstring
kind reqenum(6)comment, progress, blocker, handoff, standup, system
mentions reqobject[]
blockerobject
createdAt reqstring(date-time)
Errors — 10 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Approvals & trust

The approval inbox. Approving a gate is the ONLY path to a production write; the single-use args-hashed approval token never crosses HTTP (spec 04 §6).

GET/gates#

The approval inbox

Gates awaiting a decision (default) or already decided, filtered by ?status=.

role >= viewerX-Workspace-Id requiredcookie vb_sessionGatesController.listGates

Request

curl -sS -X GET "$VB_API/api/v1/gates" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
statusqueryenum(5)Gate status filter. Defaults to `pending`.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Matching gates (JSON array).

Gate

FieldTypeNotes
id reqstring(uuid)
gateType reqenum(9)profile.approved, promptbank.approved, program.approved, content.outline, content.publish, site.write, offsite.publish, placement.purchase, design.approve
status reqenum(5)pending, approved, rejected, expired, auto_approved
summary reqstringmin length 1
ticketId reqstring(uuid)
diffArtifactPathstring
previewUrlstring(uri)
expiresAt reqstring(date-time)
costLineMoney
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/gates/{id}#

Gate detail

One gate with its evidence payload — the diff card, claim-ledger summary or cost line the human decides on.

role >= viewerX-Workspace-Id requiredcookie vb_sessionGatesController.getGate

Request

curl -sS -X GET "$VB_API/api/v1/gates/{id}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The gate.

Gate

FieldTypeNotes
id reqstring(uuid)
gateType reqenum(9)profile.approved, promptbank.approved, program.approved, content.outline, content.publish, site.write, offsite.publish, placement.purchase, design.approve
status reqenum(5)pending, approved, rejected, expired, auto_approved
summary reqstringmin length 1
ticketId reqstring(uuid)
diffArtifactPathstring
previewUrlstring(uri)
expiresAt reqstring(date-time)
costLineMoney
Errors — 7 problem codes across 5 statuses
StatusCodetype
404gate_not_foundhttps://visiblebrand.ai/problems/gate-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/gates/{id}/approve#

Approve a gate

The single most important write in the product. Idempotent: a double-submit returns the current state with 200. Trust-tier preconditions are enforced HERE at token mint (403 tier_requirement leaves the gate pending). The minted single-use, args-hashed approval token NEVER crosses HTTP — it flows only to the Temporal signal / Gatekeeper path.

Approval is the ONLY path to a production write (invariant #1). The token is validated again at the Gatekeeper hook AND at the server layer (DECISION-04-06).

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfGatesController.approve

Request

curl -sS -X POST "$VB_API/api/v1/gates/{id}/approve" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

GateApproveRequest

FieldTypeNotes
notestring

Unknown properties are stripped rather than rejected.

Response 200 OK

The gate id and its resulting status.

GateDecisionResult

FieldTypeNotes
gateId reqstring(uuid)
status reqenum(5)pending, approved, rejected, expired, auto_approved
decidedAtstring(date-time)
decisionNotestring
Errors — 13 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403entitlement_writes_refusedhttps://visiblebrand.ai/problems/entitlement-writes-refused
404gate_not_foundhttps://visiblebrand.ai/problems/gate-not-found
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
403tier_requirementhttps://visiblebrand.ai/problems/tier-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/gates/{id}/reject#

Reject a gate

A note is required — the agent needs a reason it can act on. Idempotent like approve.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfGatesController.reject

Request

curl -sS -X POST "$VB_API/api/v1/gates/{id}/reject" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

GateRejectRequest

FieldTypeNotes
note reqstringmin length 1

Unknown properties are stripped rather than rejected.

Response 200 OK

The gate id and its resulting status.

GateDecisionResult

FieldTypeNotes
gateId reqstring(uuid)
status reqenum(5)pending, approved, rejected, expired, auto_approved
decidedAtstring(date-time)
decisionNotestring
Errors — 11 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
404gate_not_foundhttps://visiblebrand.ai/problems/gate-not-found
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Measurement

Prompt banks, metric series and evidence. Every metric carries a confidence band and its measurement regime — there are no naked numbers on this surface (DECISION-03-04).

GET/measurements/prompt-state#

Per-prompt tracking state

The workspace-wide per-prompt summary: for every prompt with scored samples, one state per engine × measurement regime — mention rate with its Wilson CI, `mentions`/`runs` (the exact numerator/denominator behind the per-question "You: 0/3" column), average rank with a band, and the modal sentiment (`mixed` on a tie, never resolved by fiat). A COMPUTED read model: recomputed from stored prompt samples on every request, backed by no table, so every figure is reproducible from the evidence drawer. States from different regimes are served separately, never blended (AC-17-02).

role >= viewerX-Workspace-Id requiredcookie vb_sessionScoresController.promptState

Request

curl -sS -X GET "$VB_API/api/v1/measurements/prompt-state" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
enginequeryenum(7)Restrict to one answer engine.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Per-prompt states grouped by prompt id, plus the tracked-prompt count.

PromptStateResponse

FieldTypeNotes
prompts reqobject[]
trackedPrompts reqinteger>= 0 · <= 9007199254740991
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/metrics/citation-share#

Citation share

The share of cited sources that belong to the brand's own properties. Every point carries a confidence interval, the sample size n and the measurement regime (runner_mode + model_version). Points from different regimes are never merged into one aggregate (AC-17-02).

role >= viewerX-Workspace-Id requiredcookie vb_sessionMetricsController.citationShare

Request

curl -sS -X GET "$VB_API/api/v1/metrics/citation-share" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
enginequeryenum(7)Restrict to one answer engine. Absent returns one series per engine.
fromquerystring(date)Inclusive start date (YYYY-MM-DD).
toquerystring(date)Inclusive end date (YYYY-MM-DD).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

One series per engine (JSON array).

MetricSeries

FieldTypeNotes
metric req"visibility" | "sov" | "citation_share" | "position_score"
engine reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
stratumobject
points reqobject[]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/metrics/position#

Position score

Reciprocal-rank position of the brand inside the answer. Every point carries a confidence interval, the sample size n and the measurement regime (runner_mode + model_version). Points from different regimes are never merged into one aggregate (AC-17-02).

role >= viewerX-Workspace-Id requiredcookie vb_sessionMetricsController.position

Request

curl -sS -X GET "$VB_API/api/v1/metrics/position" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
enginequeryenum(7)Restrict to one answer engine. Absent returns one series per engine.
fromquerystring(date)Inclusive start date (YYYY-MM-DD).
toquerystring(date)Inclusive end date (YYYY-MM-DD).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

One series per engine (JSON array).

MetricSeries

FieldTypeNotes
metric req"visibility" | "sov" | "citation_share" | "position_score"
engine reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
stratumobject
points reqobject[]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/metrics/sov#

Share of voice

Brand mentions as a share of all brand mentions in the answer set. Every point carries a confidence interval, the sample size n and the measurement regime (runner_mode + model_version). Points from different regimes are never merged into one aggregate (AC-17-02).

role >= viewerX-Workspace-Id requiredcookie vb_sessionMetricsController.sov

Request

curl -sS -X GET "$VB_API/api/v1/metrics/sov" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
enginequeryenum(7)Restrict to one answer engine. Absent returns one series per engine.
fromquerystring(date)Inclusive start date (YYYY-MM-DD).
toquerystring(date)Inclusive end date (YYYY-MM-DD).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

One series per engine (JSON array).

MetricSeries

FieldTypeNotes
metric req"visibility" | "sov" | "citation_share" | "position_score"
engine reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
stratumobject
points reqobject[]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/metrics/visibility#

Visibility over time

How often the brand appears in answers to unbranded questions. Every point carries a confidence interval, the sample size n and the measurement regime (runner_mode + model_version). Points from different regimes are never merged into one aggregate (AC-17-02).

role >= viewerX-Workspace-Id requiredcookie vb_sessionMetricsController.visibility

Request

curl -sS -X GET "$VB_API/api/v1/metrics/visibility" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
enginequeryenum(7)Restrict to one answer engine. Absent returns one series per engine.
fromquerystring(date)Inclusive start date (YYYY-MM-DD).
toquerystring(date)Inclusive end date (YYYY-MM-DD).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

One series per engine (JSON array).

MetricSeries

FieldTypeNotes
metric req"visibility" | "sov" | "citation_share" | "position_score"
engine reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
stratumobject
points reqobject[]
Example response
[
  {
    "metric": "visibility",
    "engine": "chatgpt",
    "points": [
      {
        "date": "2026-08-10",
        "value": 0.34,
        "ci": {
          "low": 0.21,
          "high": 0.49
        },
        "n": 42,
        "regime": {
          "runnerMode": "api",
          "modelVersion": "gpt-5-2026-06"
        }
      }
    ]
  }
]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/prompt-banks#

List prompt banks

Draft and frozen banks for the workspace, optionally filtered by locale. AR and EN banks are siblings, never merged. Each card carries `promptCount` so a review page needs no per-bank round trip to show its size.

role >= viewerX-Workspace-Id requiredcookie vb_sessionPromptBanksController.list

Request

curl -sS -X GET "$VB_API/api/v1/prompt-banks" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
localequery"ar" | "en"Filter to one locale.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The workspace's banks (JSON array).

PromptBank

FieldTypeNotes
id reqstring(uuid)
version reqinteger<= 9007199254740991
status req"draft" | "approved" | "frozen" | "superseded"
locale req"ar" | "en"
promptCountinteger>= 0 · <= 9007199254740991
enabledCountinteger>= 0 · <= 9007199254740991
createdAtstring(date-time)
frozenAtstring(date-time)
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/prompt-banks/{id}/approve#

Approve and freeze a bank

Lint precondition then freeze. A brand leak in an unbranded prompt answers 422 prompt_bank_lint_failed with the offending prompts as RFC-7807 extension members (AC-02-03) — the bank never freezes with a leak in it. FREEZING STARTS THE WORK: on the draft → frozen transition the first measurement over that version is enqueued through the same producer POST /runs uses, and `measurement` reports the queue receipt (or, when it could not start, the named precondition it waits on). The enqueue is contained (a queue outage cannot un-freeze the bank) and idempotent (a deterministic version-scoped jobId), so `measurement` is absent when an already-frozen bank is re-approved — nothing was started.

The 422 `prompt_bank_lint_failed` problem carries the EVIDENCE as RFC-7807 extension members, so a review page can highlight the offending prompts instead of restating the message: `brandLeaks` (integer — how many findings are brand leaks, the DECISION-03-01 violations) and `violations` (array of `{promptId, text, violations: [{code, message, alias?}]}`, where `code` is one of `brand_leak`, `over_length`, `empty_text`, `control_class_missing_brand` and `alias` is present on a brand leak, naming the alias that matched).

The measurement kicked off by the freeze is enqueued with a DETERMINISTIC jobId scoped to workspace + locale + bank version, so replaying the approval cannot buy a second vendor-billed cycle, while freezing a NEW version does get its own baseline run.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfPromptBanksController.approve

Request

curl -sS -X POST "$VB_API/api/v1/prompt-banks/{id}/approve" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The frozen bank id, version, status and what the freeze kicked off.

BankApproveResponse

FieldTypeNotes
bankId reqstring(uuid)
version reqinteger<= 9007199254740991
status reqstring
measurementobject
Errors — 12 problem codes across 7 statuses
StatusCodetype
404bank_not_foundhttps://visiblebrand.ai/problems/bank-not-found
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422empty_bankhttps://visiblebrand.ai/problems/empty-bank
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
422prompt_bank_lint_failedhttps://visiblebrand.ai/problems/prompt-bank-lint-failed
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/prompt-banks/{id}/prompts#

Bank contents

The bank's prompts grouped by prompt class, with its version and freeze status, its counts (total/enabled/per class/per language) and its STRATIFY coverage: target, actual and deficit per class × locale (spec 03 §3 step 4). The targets are allocated by the same domain quota matrix the bank BUILD ran, against the plan's prompt-bank capacity — `coverage.basis` says whether that size came from the workspace entitlement or the conservative Starter fallback, so no client has to invent a target. `coverage.totalActual` counts stratum members only: an AR prompt inside an EN bank fills no EN stratum and is reported through `counts.byLanguage` instead. ADDITIVE `tracking` per prompt: one state per engine × measurement regime the prompt has scored samples under (empty until the first measurement), each carrying `mentions`/`runs` with the Wilson CI band — this is what makes the per-question "You: 0/3" column buildable directly from this read. States from different regimes are never merged (AC-17-02); everything is recomputed from stored samples, no table backs it.

role >= viewerX-Workspace-Id requiredcookie vb_sessionPromptBanksController.prompts

Request

curl -sS -X GET "$VB_API/api/v1/prompt-banks/{id}/prompts" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The bank, its grouped prompts (with per-prompt tracking), counts and coverage.

BankPromptsResponse

FieldTypeNotes
bankId reqstring(uuid)
version reqinteger<= 9007199254740991
status req"draft" | "approved" | "frozen" | "superseded"
locale req"ar" | "en"
groups reqobject[]
counts reqobject
coverage reqobject
Errors — 7 problem codes across 5 statuses
StatusCodetype
404bank_not_foundhttps://visiblebrand.ai/problems/bank-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

PATCH/prompt-banks/{id}/prompts/{pid}#

Edit a draft prompt

Draft banks only — an edit against a frozen bank is a 409 frozen_bank, because a frozen bank is what makes a measurement comparable over time. Text is capped at 500 characters, mirroring the runner hard cap (AC-17-04).

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfPromptBanksController.patchPrompt

Request

curl -sS -X PATCH "$VB_API/api/v1/prompt-banks/{id}/prompts/{pid}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
pid reqpathstringPrompt id (uuid).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

PatchPromptRequest

FieldTypeNotes
textstringmin length 1 · max length 500
enabledboolean

Unknown properties are stripped rather than rejected.

Response 200 OK

The updated prompt.

Prompt

FieldTypeNotes
id reqstring(uuid)
class reqenum(11)discovery, recommendation, comparison, alternative, problem_solution, long_tail_intent, persona, local, use_case, attribute, branded_control
text reqstringmin length 1 · max length 500
language req"ar" | "en"
dialectTagstring
countrystring
funnel req"tofu" | "mofu" | "bofu"
priority req"head" | "tail"
enabled reqboolean
Errors — 12 problem codes across 6 statuses
StatusCodetype
404bank_not_foundhttps://visiblebrand.ai/problems/bank-not-found
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409frozen_bankhttps://visiblebrand.ai/problems/frozen-bank
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404prompt_not_foundhttps://visiblebrand.ai/problems/prompt-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/prompt-banks/{id}/prompts/bulk#

Bulk enable/disable prompts

The throughput verb the review page needs at 400 prompts — enable or disable up to 500 prompts in one call, instead of one PATCH each. Semantics mirror the single-prompt PATCH exactly: DRAFT banks only (a frozen bank answers 409 frozen_bank — the frozen version is a measurement regime and must not shift under it), and the whole batch validates BEFORE anything applies: an unknown prompt id fails the entire request with the offending ids named, never a half-applied batch. Idempotent in effect — `updated` counts only prompts whose state actually changed, so a double-submit reports 0.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfPromptBanksController.bulk

Request

curl -sS -X POST "$VB_API/api/v1/prompt-banks/{id}/prompts/bulk" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

BulkPromptsRequest

FieldTypeNotes
promptIds reqstring(uuid)[]min 1 items · max 500 items
action req"enable" | "disable"

Unknown properties are stripped rather than rejected.

Response 200 OK

How many distinct prompts were named and how many actually changed.

BulkPromptsResponse

FieldTypeNotes
bankId reqstring(uuid)
action req"enable" | "disable"
requested reqinteger<= 9007199254740991
updated reqinteger>= 0 · <= 9007199254740991
Errors — 12 problem codes across 6 statuses
StatusCodetype
404bank_not_foundhttps://visiblebrand.ai/problems/bank-not-found
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409frozen_bankhttps://visiblebrand.ai/problems/frozen-bank
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404prompt_not_foundhttps://visiblebrand.ai/problems/prompt-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/prompt-banks/build#

Build a prompt bank

Runs candidates through the deterministic AR-aware linter, dedupe and quota stratification. The response reports what was KEPT and what was DROPPED with the reason, so nothing disappears silently.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfPromptBanksController.build

Request

curl -sS -X POST "$VB_API/api/v1/prompt-banks/build" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

BuildBankRequest

FieldTypeNotes
locale req"ar" | "en"
planSize reqinteger<= 9007199254740991
classSharesmap
candidates reqobject[]min 1 items
brandAliases reqstring[]min 1 items
competitorAliasesstring[]

Unknown properties are stripped rather than rejected.

Response 201 Created

The built bank with kept/dropped/coverage detail.

BuildPromptBankResponse

FieldTypeNotes
ok req"true"
bankId reqstring(uuid)
version reqinteger<= 9007199254740991
kept reqobject[]
dropped reqobject
coverage reqobject[]
shareViolations reqobject[]
Errors — 11 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422empty_bankhttps://visiblebrand.ai/problems/empty-bank
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/runs#

List measurement runs

Runs newest first, for the evidence drawer. Without DATABASE_URL only the literal `demo` pseudo-run exists; with it, real pipeline runs are served.

role >= viewerX-Workspace-Id requiredcookie vb_sessionRunsController.list

Request

curl -sS -X GET "$VB_API/api/v1/runs" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
bankIdquerystring(uuid)Restrict to runs against one prompt bank.
cursorquerystringKeyset cursor from the previous page's nextCursor (a run id, or `demo` in in-memory mode).
limitqueryintegerPage size, 1–100. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of runs (JSON array).

RunSummary

FieldTypeNotes
id reqstringmin length 1
bankId reqstring(uuid)
engineKey reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
status reqenum(5)queued, running, partial, complete, failed
samplesRequested reqinteger>= 0 · <= 9007199254740991
samplesCompleted reqinteger>= 0 · <= 9007199254740991
createdAt reqstring(date-time)
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/runs/{runId}/samples#

Raw evidence samples

The individual engine answers behind an aggregate — the receipts. Each sample carries its verbatim answer text, the classified citations and the regime it was collected under.

role >= viewerX-Workspace-Id requiredcookie vb_sessionRunsController.samples

Request

curl -sS -X GET "$VB_API/api/v1/runs/{runId}/samples" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
runId reqpathstringMeasurement run id (uuid), or the literal `demo` in in-memory mode.
prompt_idquerystring(uuid)Restrict to samples for one prompt.
cursorquerystring(uuid)Keyset cursor: the id of the last sample from the previous page. Absent starts at the newest row.
limitqueryintegerPage size, 1–200. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of samples (JSON array).

Sample

FieldTypeNotes
id reqstring(uuid)
promptId reqstring(uuid)
answerText reqstring
citations reqCitation[]
mentioned reqboolean
positioninteger<= 9007199254740991
sentimentinteger>= -100 · <= 100
runnerMode reqenum(5)api_model, api_model_websearch, consumer_surface, direct_api, fal_llm
modelVersion reqstring
Errors — 8 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404run_not_foundhttps://visiblebrand.ai/problems/run-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/scores#

Composite scores (AVS / CRS / Share of Model)

The ADR 0004 composites: AVS (Σ PVS × mention_rate / Σ PVS) and Share of Model per engine × measurement regime — never cross-engine averaged (AC-17-02) — each with a CI band propagated from the per-prompt Wilson bands, and CRS = AccessGate × (0.45 OnSite + 0.55 OffSite) over the findings feed, where AccessGate is 0.2 exactly when an open access-gate marker finding (a T2 crawler block) exists. Nothing is stored: every number is recomputed on read and the response carries the full drill-down — which prompts carry which PVS (with the versioned weight set and the NAMED neutral-prior components) and which findings cost which CRS points (AC-8) — so every figure is recomputable by hand.

role >= viewerX-Workspace-Id requiredcookie vb_sessionScoresController.list

Request

curl -sS -X GET "$VB_API/api/v1/scores" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
enginequeryenum(7)Restrict AVS/SoM to one answer engine (CRS is workspace-level).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

One AVS/SoM entry per engine × regime, plus the workspace CRS.

ScoresResponse

FieldTypeNotes
weightSet reqstringmin length 1
engines reqobject[]
crs reqobject
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Findings

The deterministic detector feed, each finding linked to the playbook ticket it created.

GET/findings#

The findings feed

Deterministic detector output, newest first. Detectors are code, never model judgement (DECISION-17-03), and each finding links the playbook ticket it created.

role >= viewerX-Workspace-Id requiredcookie vb_sessionFindingsController.list

Request

curl -sS -X GET "$VB_API/api/v1/findings" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
cursorquerystring(uuid)Keyset cursor: the id of the last finding from the previous page. Absent starts at the newest row.
limitqueryintegerPage size, 1–100. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of findings.

FindingsPage

FieldTypeNotes
items reqFinding[]
nextCursor reqstring | null
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Demand seeds

Mined demand signals (autocomplete suggestions, People-Also-Ask questions) with checkable evidence, and per-prompt provenance — no prompt without a source (AC-2); template-derived prompts say so instead of inventing one.

GET/seeds#

Mined demand seeds

The stored demand signals mined at profile approval, newest first, each carrying its source (Google Autocomplete per market, DataForSEO People-Also-Ask / related searches), quote-level evidence and a deterministic confidence rung. `metricVolume` is absent for every implemented source — no volume vendor is wired, and absent means unknown, never 0. The reserved `gsc` source stays empty until the Search Console data reader lands (the GSC adapter is consent plumbing only today).

role >= viewerX-Workspace-Id requiredcookie vb_sessionSeedsController.list

Request

curl -sS -X GET "$VB_API/api/v1/seeds" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
sourcequery"autocomplete_google" | "dataforseo_paa" | "dataforseo_related" | "gsc"Filter to one demand source.
languagequery"ar" | "en"Filter to one language.
cursorquerystring(uuid)Keyset cursor: the id of the last seed from the previous page. Absent starts at the newest row.
limitqueryintegerPage size, 1–100. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of seeds.

SeedsPage

FieldTypeNotes
items reqSeed[]
nextCursor reqstring | null
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/seeds/prompts/{promptId}#

A prompt's provenance

Where one prompt came from (AC-2: no prompt without a source). `derivation: 'seeds'` lists the stored demand seeds the prompt was built from; `derivation: 'template'` is the honest answer for prompts built by the deterministic template fallback — an empty lineage, never an invented one.

role >= viewerX-Workspace-Id requiredcookie vb_sessionSeedsController.promptProvenance

Request

curl -sS -X GET "$VB_API/api/v1/seeds/prompts/{promptId}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
promptId reqpathstringpromptId path parameter.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The derivation label with the linked seeds.

PromptProvenanceResponse

FieldTypeNotes
promptId reqstring(uuid)
derivation req"seeds" | "template"
seedIds reqstring(uuid)[]
seeds reqSeed[]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404prompt_not_foundhttps://visiblebrand.ai/problems/prompt-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Reporting

The weekly digest. Metric lines are copied verbatim from the same service /metrics answers with (AC-02-08).

GET/reports/weekly/latest#

This week's digest

The current ISO week's digest, composed on demand and cached per (workspace, week). Metric lines are copied VERBATIM from the same MetricsService instance /metrics uses, so the digest and the dashboard can never disagree about a number (AC-02-08).

role >= viewerX-Workspace-Id requiredcookie vb_sessionReportsController.latest

Request

curl -sS -X GET "$VB_API/api/v1/reports/weekly/latest" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The digest.

WeeklyDigest

FieldTypeNotes
workspaceId reqstring(uuid)
period reqobject
headline reqBilingualText
metrics reqobject[]
wins reqobject[]
activity reqobject
nextUp reqobject[]
generatedAt reqstring(date-time)
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Research

Versioned, append-only market briefs. The api serves stored versions and never composes brief content.

GET/market-brief/latest#

Latest market brief

The newest stored brief version with its full markdown. The api serves stored artifacts; the workers pipeline owns writes.

role >= viewerX-Workspace-Id requiredcookie vb_sessionResearchController.latest

Request

curl -sS -X GET "$VB_API/api/v1/market-brief/latest" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The newest brief version.

MarketBrief

FieldTypeNotes
version reqinteger>= 1 · <= 9007199254740991
summary reqstring | null
generatedAt reqstring(date-time)
generatedBy reqstring | null
contentMd reqstring | null
notestring
Errors — 8 problem codes across 5 statuses
StatusCodetype
400artifact_path_outside_roothttps://visiblebrand.ai/problems/artifact-path-outside-root
404brief_not_foundhttps://visiblebrand.ai/problems/brief-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/market-brief/versions#

Brief version list

Every stored version with its summary and generation timestamp — briefs are append-only, so this is a history, not a mutable list.

role >= viewerX-Workspace-Id requiredcookie vb_sessionResearchController.versions

Request

curl -sS -X GET "$VB_API/api/v1/market-brief/versions" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The version list (JSON array).

MarketBriefVersion

FieldTypeNotes
version reqinteger>= 1 · <= 9007199254740991
summary reqstring | null
generatedAt reqstring(date-time)
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/market-brief/versions/{version}#

A pinned brief version

One version by its integer number, with full markdown. What the web version picker fetches.

role >= viewerX-Workspace-Id requiredcookie vb_sessionResearchController.byVersion

Request

curl -sS -X GET "$VB_API/api/v1/market-brief/versions/{version}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
version reqpathintegerMarket-brief version number (1-based, append-only).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The requested version.

MarketBrief

FieldTypeNotes
version reqinteger>= 1 · <= 9007199254740991
summary reqstring | null
generatedAt reqstring(date-time)
generatedBy reqstring | null
contentMd reqstring | null
notestring
Errors — 8 problem codes across 5 statuses
StatusCodetype
400artifact_path_outside_roothttps://visiblebrand.ai/problems/artifact-path-outside-root
404brief_not_foundhttps://visiblebrand.ai/problems/brief-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Integrations

Connect / credential / revoke / health lifecycle. Credential values cross exactly one seam into the vault and are never echoed back (DECISION-06-01).

GET/integrations#

List integrations

Provider catalog with per-provider status. Credential values are never present in this response.

role >= viewerX-Workspace-Id requiredcookie vb_sessionIntegrationsController.list

Request

curl -sS -X GET "$VB_API/api/v1/integrations" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Providers and their connection status.

Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

DELETE/integrations/{id}#

Revoke an integration

Kills the vault entry, flips the row status and writes an audit entry.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfIntegrationsController.revoke

Request

curl -sS -X DELETE "$VB_API/api/v1/integrations/{id}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 204 No Content

Revoked.

Errors — 9 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
404integration_not_foundhttps://visiblebrand.ai/problems/integration-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/integrations/{id}/health#

Integration health

Last check outcome and the error text when the credential has gone bad — honest states, not a green light by default.

role >= viewerX-Workspace-Id requiredcookie vb_sessionIntegrationsController.health

Request

curl -sS -X GET "$VB_API/api/v1/integrations/{id}/health" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Current health.

IntegrationHealth

FieldTypeNotes
status req"pending" | "connected" | "error" | "revoked"
lastCheck reqstring(date-time) | null
error reqstring | null
Errors — 7 problem codes across 5 statuses
StatusCodetype
404integration_not_foundhttps://visiblebrand.ai/problems/integration-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/integrations/{id}/refresh#

Refresh an OAuth token

Exercises the stored refresh token. A Google `invalid_grant` (expired or revoked per their docs) flips the row to `error` and answers 409 oauth_refresh_invalid_grant with a re-auth notification — never a silent retry loop.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfIntegrationRefreshController.refresh

Request

curl -sS -X POST "$VB_API/api/v1/integrations/{id}/refresh" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 204 No Content

Refreshed.

Errors — 13 problem codes across 7 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409integration_not_connectedhttps://visiblebrand.ai/problems/integration-not-connected
404integration_not_foundhttps://visiblebrand.ai/problems/integration-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
409oauth_refresh_invalid_granthttps://visiblebrand.ai/problems/oauth-refresh-invalid-grant
502oauth_upstream_unavailablehttps://visiblebrand.ai/problems/oauth-upstream-unavailable
400refresh_not_supportedhttps://visiblebrand.ai/problems/refresh-not-supported
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/integrations/{provider}/connect#

Begin a connection

Answers either `{kind:"oauth", oauthUrl}` for the guided Google consent flows or `{kind:"credentials", formSchema, reason}` for scoped-token providers. A provider that exists in the enum but has no v1 flow answers an honest 501 provider_not_connectable rather than an invented URL.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfIntegrationsController.connect

Request

curl -sS -X POST "$VB_API/api/v1/integrations/{provider}/connect" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
provider reqpathstringIntegration provider key, e.g. `cloudflare`, `gsc`, `ga4`.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

How to complete the connection.

ConnectResponse

One of 2 variants:

oauth
FieldTypeNotes
kind req"oauth"
oauthUrl reqstring(uri)
credentials
FieldTypeNotes
kind req"credentials"
formSchema reqobject
reason reqBilingualText
downloadPathstring
pairingSecretPathstring
stepsBilingualText[]
Errors — 9 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
501provider_not_connectablehttps://visiblebrand.ai/problems/provider-not-connectable
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/integrations/{provider}/credentials#

Submit credentials

Values travel TLS → here → vault and are NEVER echoed, logged, stored in Postgres or attached to an event (DECISION-06-01). Hence 204 with an empty body, always.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfIntegrationsController.submitCredentials

Request

curl -sS -X POST "$VB_API/api/v1/integrations/{provider}/credentials" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
provider reqpathstringIntegration provider key, e.g. `cloudflare`, `gsc`, `ga4`.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

CredentialsSubmitRequest

FieldTypeNotes
values reqmap

Unknown properties are stripped rather than rejected.

Response 204 No Content

Stored in the vault. No body, by design.

Errors — 11 problem codes across 7 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
501provider_not_connectablehttps://visiblebrand.ai/problems/provider-not-connectable
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/integrations/oauth/google/callback#

GSC/GA4 consent callback

The CONSENT flow's own callback, distinct from the login callback. `state` binds {workspaceId, provider} server-side, so this route carries no workspace header and no session requirement — it is a browser redirect from accounts.google.com. Every post-state outcome answers 302 back to the web settings page.

no workspace rolenot workspace-scopedunauthenticatedGoogleConsentCallbackController.callback

Request

curl -sS -X GET "$VB_API/api/v1/integrations/oauth/google/callback"

Parameters

NameInTypeDescription
statequerystringThe bound single-use state.
codequerystringGoogle authorization code.
errorquerystringPresent when the user denied consent.

Response 200 OK

302 back to the web app settings page.

Errors — 3 problem codes across 3 statuses
StatusCodetype
502oauth_exchange_failedhttps://visiblebrand.ai/problems/oauth-exchange-failed
400oauth_state_mismatchhttps://visiblebrand.ai/problems/oauth-state-mismatch
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/integrations/wordpress/pairing-secret#

Mint the WordPress pairing secret

Generates the per-workspace shared HMAC secret the WordPress pairing steps reference: the user pastes it into the plugin under Settings → VisibleBrand AND into the `pairingCode` credential field; the live handshake then proves both sides hold the same secret. The value is returned ONCE in this response (the shown-once posture of an Application Password) and stored in the vault; it is a platform-generated secret delivered TO the user, not an echoed submission, so DECISION-06-01 is untouched. Re-minting rotates: the superseded vault entry dies immediately.

The handshake — not a comparison against the stored copy — is what proves possession, so a secret minted elsewhere (e.g. WP-CLI) still pairs.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfWordPressPluginController.mintPairingSecret

Request

curl -sS -X POST "$VB_API/api/v1/integrations/wordpress/pairing-secret" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The freshly minted secret and its mint time. Shown once.

WordPressPairingSecret

FieldTypeNotes
secret reqstringpattern ^[0-9a-f]{64}$
mintedAt reqstring(date-time)
Errors — 8 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/integrations/wordpress/plugin#

Download the WordPress connector plugin

The `visiblebrand-connector` plugin zip, built at request time from the checked-in plugin source that ships with the deploy — no separate artifact store, so the download can never lag the code. Deterministic: identical source yields a byte-identical zip (store method, fixed timestamps), and the version in the filename is parsed from the plugin header — the single source of truth. The zip contains only what a WordPress install needs (main file, src/, readme.txt, license.txt), rooted so wp-admin Upload Plugin extracts it correctly.

Step 1 of the connect flow the wordpress connect descriptor describes (its `downloadPath` points here); the full guide is docs/integrations/wordpress.md.

role >= viewerX-Workspace-Id requiredcookie vb_sessionWordPressPluginController.downloadPlugin

Request

curl -sS -X GET "$VB_API/api/v1/integrations/wordpress/plugin" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The plugin as `application/zip`, served as an attachment named `visiblebrand-connector-{version}.zip`.

string

Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Settings

The autonomy dial. Defaults are conservative; changing it is owner-only and audited.

GET/settings/autonomy#

Read the autonomy dial

Per-action-type autonomy policy. Defaults are conservative.

role >= viewerX-Workspace-Id requiredcookie vb_sessionSettingsController.getAutonomy

Request

curl -sS -X GET "$VB_API/api/v1/settings/autonomy" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The current policy.

AutonomySettings

FieldTypeNotes
policies reqobject[]
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

PATCH/settings/autonomy#

Change the autonomy dial

Owner-only and audited — widening what agents may do without asking is an owner decision, and the change is an audit event.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfSettingsController.patchAutonomy

Request

curl -sS -X PATCH "$VB_API/api/v1/settings/autonomy" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

PatchAutonomyRequest

FieldTypeNotes
policies reqobject[]min 1 items

Unknown properties are stripped rather than rejected.

Response 204 No Content

Applied. No body.

Errors — 10 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Notifications

The unified inbox and the per-user preference matrix (type × channel) with quiet hours.

GET/notification-prefs#

Read the preference matrix

The FULL type × channel matrix, with defaults filled in when unset, plus quiet hours.

role >= viewerX-Workspace-Id requiredcookie vb_sessionNotificationPrefsController.get

Request

curl -sS -X GET "$VB_API/api/v1/notification-prefs" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The caller's preferences.

Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

PUT/notification-prefs#

Replace the preference matrix

Exhaustive by contract — every type × every channel. A partial body is a 400, never a silent merge, so a client can never half-configure someone's routing. The in-app row is never suppressed, quiet hours included.

role >= viewerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfNotificationPrefsController.put

Request

curl -sS -X PUT "$VB_API/api/v1/notification-prefs" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

Response 204 No Content

Replaced. No body.

Errors — 9 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/notifications#

The inbox

The REQUESTER'S OWN rows only — scoping is by authenticated actor, so no cross-user read exists by construction.

role >= viewerX-Workspace-Id requiredcookie vb_sessionNotificationsController.list

Request

curl -sS -X GET "$VB_API/api/v1/notifications" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
cursorquerystringKeyset cursor from the previous page's nextCursor (a notification cursor).
limitqueryintegerPage size, 1–100. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of notifications.

NotificationInboxPage

FieldTypeNotes
items reqNotification[]
nextCursor reqstring | null
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/notifications/{id}/read#

Mark read

Idempotent. Another user's row answers 404, identical to an unknown id.

role >= viewerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfNotificationsController.markRead

Request

curl -sS -X POST "$VB_API/api/v1/notifications/{id}/read" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 204 No Content

Marked read.

Errors — 9 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404notification_not_foundhttps://visiblebrand.ai/problems/notification-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Billing & plans

Plan card, entitlements snapshot, usage meters and the hire-an-agent mechanic.

POST/agents/{agentKey}/hire#

Hire an agent

Owner-only — hiring changes what the org pays for. The response distinguishes the four AC-15-02 outcomes: already included, purchasable as an add-on (with the hold), walled by the plan, or refused because a trial plan may never execute production writes.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfHireAgentController.hire

Request

curl -sS -X POST "$VB_API/api/v1/agents/{agentKey}/hire" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
agentKey reqpathstringAgent registry key, e.g. `technical`, `content`, `researcher`.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The hire outcome.

HireAgentResponse

FieldTypeNotes
status req"hired" | "requires_addon" | "already_hired"
addonenum(7)agent_content, agent_offsite, agent_topic, agent_builder, extra_prompts_100, extra_engine, extra_workspace
Errors — 10 problem codes across 6 statuses
StatusCodetype
501agent_not_availablehttps://visiblebrand.ai/problems/agent-not-available
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403entitlement_writes_refusedhttps://visiblebrand.ai/problems/entitlement-writes-refused
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/plan#

Plan, entitlements and usage

The plan card, the entitlements snapshot the server enforces (AC-15-02) and the usage meters. Viewer-readable: the team panel and every capability wall render from this one response.

role >= viewerX-Workspace-Id requiredcookie vb_sessionPlanController.plan

Request

curl -sS -X GET "$VB_API/api/v1/plan" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Plan summary, entitlements and meters.

PlanResponse

FieldTypeNotes
plan reqobject
entitlements reqobject
usage reqobject[]
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Measurement costs

What measurement spends (PM AC-12): a pre-run cost preview priced only from vendor-cited figures (unknown stays unknown — never guessed) and the workspace month-to-date agent_costs sums, integer minor units per currency.

GET/costs/month#

Month-to-date measurement spend

The workspace's current-UTC-calendar-month agent_costs sums — the same ledger the workers' cost meter writes (spec 17 §5) and the monthly budget ceiling is checked against (PM AC-12). Integer minor units, one total per currency, never blended.

role >= viewerX-Workspace-Id requiredcookie vb_sessionCostsController.month

Request

curl -sS -X GET "$VB_API/api/v1/costs/month" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Half-open month period and per-currency totals.

MonthCostResponse

FieldTypeNotes
period reqobject
totals reqMoney[]
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/costs/preview#

Preview a measurement run's cost

Estimated vendor calls (promptCount × samples per engine) and a cost band from a versioned unit-price table. Prices come ONLY from the vendor manifest's cited figures (Docs-First Protocol, spec 18); an engine without a citable price answers null with the reason, and the band's upper bound is null while any requested engine is unpriced — unknown is served as unknown, never as a guess.

role >= viewerX-Workspace-Id requiredcookie vb_sessionCostsController.preview

Request

curl -sS -X GET "$VB_API/api/v1/costs/preview" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
promptCount reqqueryintegerEnabled prompts the run would fan out.
engines reqquerystringComma-separated engine keys (e.g. `chatgpt,gemini`).
samples reqqueryintegerSamples per prompt per engine (n).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Per-engine estimates and the honest total band.

CostPreviewResponse

FieldTypeNotes
priceTableVersion reqstringmin length 1
promptCount reqinteger>= 1 · <= 9007199254740991
samples reqinteger>= 1 · <= 9007199254740991
estimatedCalls reqinteger>= 0 · <= 9007199254740991
engines reqobject[]
range reqobject
unknownEngines reqenum(7)[]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Placements & network

The Mawdoo3 network catalog and placement lifecycle. Money moves only after a human approves the placement.purchase gate.

GET/network/properties#

Network property catalog

Placement inventory across the Mawdoo3 network, filterable by vertical and locale. Prices are integer minor units with an ISO-4217 currency; `null` means the property does not offer that placement kind.

role >= viewerX-Workspace-Id requiredcookie vb_sessionNetworkPropertiesController.listProperties

Request

curl -sS -X GET "$VB_API/api/v1/network/properties" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
verticalquerystringFilter by vertical.
localequerystringFilter by locale.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Active catalog properties.

NetworkPropertiesResponse

FieldTypeNotes
items reqobject[]
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/placements#

List placements

The workspace's placement ledger with status and cost lines.

role >= viewerX-Workspace-Id requiredcookie vb_sessionPlacementsController.list

Request

curl -sS -X GET "$VB_API/api/v1/placements" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The ledger.

PlacementsListResponse

FieldTypeNotes
items reqPlacementDto[]
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/placements/{id}/approve#

Request placement approval

Requests the placement.purchase gate on the placement's ticket — it does NOT spend money. The human then decides in the approval inbox; the T2 tier requirement is enforced at token mint, and a fresh approval places the billing hold via the gate observer. Idempotent: an existing pending gate answers `replayed: true`.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfPlacementsController.approve

Request

curl -sS -X POST "$VB_API/api/v1/placements/{id}/approve" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The gate awaiting a human decision.

ApprovePlacementResponse

FieldTypeNotes
gate reqGate
replayed reqboolean
Errors — 10 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
404placement_not_foundhttps://visiblebrand.ai/problems/placement-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/placements/propose#

Propose a placement

Creates the board ticket and the placement row plus the complete proposal card (property, audience, rationale, disclosure, cost, expected impact with a CI — AC-02-06, populated by construction). NO gate and NO money yet.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfPlacementsController.propose

Request

curl -sS -X POST "$VB_API/api/v1/placements/propose" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

ProposePlacementRequest

FieldTypeNotes
propertyKey reqstringmin length 1 · max length 200
kind req"onetime" | "weekly_series"
briefMd reqstringmin length 1

Unknown properties are stripped rather than rejected.

Response 201 Created

The proposed placement and its brief.

ProposePlacementResponse

FieldTypeNotes
placement reqPlacementDto
briefMd reqstring
Errors — 11 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
422placement_kind_unavailablehttps://visiblebrand.ai/problems/placement-kind-unavailable
404property_not_foundhttps://visiblebrand.ai/problems/property-not-found
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Trust tiers & verification

Domain verification (T1) and the manual KYB queue (T2). Trust tier is enforced at approval-token mint, not here.

GET/verification#

Verification status

The workspace's trust tier and any pending domain claim. Reflects t2 when the manual KYB review has granted it.

role >= viewerX-Workspace-Id requiredcookie vb_sessionVerificationController.status

Request

curl -sS -X GET "$VB_API/api/v1/verification" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

Tier and claim state.

VerificationStatusResponse

FieldTypeNotes
tier req"t0" | "t1" | "t2"
domain reqstring | null
method req"dns" | "file" | "connector" | "gsc" | null
verifiedAt reqstring(date-time) | null
pending reqobject | null
Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/verification/check#

Check a domain claim

200 EITHER WAY: `{verified:false, failure}` is a flow answer (AC-23-03), not an error — RFC-7807 is reserved for request faults, so a not-yet-propagated DNS record is not dressed up as a 4xx. 409 only when there is no pending claim to check.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfVerificationController.check

Request

curl -sS -X POST "$VB_API/api/v1/verification/check" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The check outcome, verified or not.

CheckVerificationResponse

One of 2 variants:

true
FieldTypeNotes
verified req"true"
tier req"t0" | "t1" | "t2"
false
FieldTypeNotes
verified req"false"
failure reqobject
Errors — 9 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409no_pending_verificationhttps://visiblebrand.ai/problems/no-pending-verification
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/verification/kyb/decide#

Decide a KYB review (platform admin)

Approve flips the workspace t1 → t2 with events and an audit entry; reject records the event. The reviewer must always justify the decision — the note is required. The body names its target workspace because this route is not scoped by header.

no workspace rolenot workspace-scopedheader x-vb-admin-tokenKybController.decide

Request

curl -sS -X POST "$VB_API/api/v1/verification/kyb/decide" \
  -H "x-vb-admin-token: $VB_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

KybDecideRequest

FieldTypeNotes
workspaceId reqstring(uuid)
decision req"approve" | "reject"
note reqstringmin length 1

Unknown properties are stripped rather than rejected.

Response 204 No Content

Recorded.

Errors — 5 problem codes across 6 statuses
StatusCodetype
503admin_disabledhttps://visiblebrand.ai/problems/admin-disabled
401admin_token_invalidhttps://visiblebrand.ai/problems/admin-token-invalid
409no_pending_kybhttps://visiblebrand.ai/problems/no-pending-kyb
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/verification/kyb/queue#

KYB review queue (platform admin)

PLATFORM-ADMIN route, deliberately NOT workspace-scoped: the reviewer works across workspaces. Authority is the x-vb-admin-token shared secret, compared in constant time. Unset VB_ADMIN_TOKEN means the surface is OFF (503), never open.

no workspace rolenot workspace-scopedheader x-vb-admin-tokenKybController.queue

Request

curl -sS -X GET "$VB_API/api/v1/verification/kyb/queue" \
  -H "x-vb-admin-token: $VB_ADMIN_TOKEN"

Response 200 OK

Pending reviews, oldest first.

KybQueueResponse

FieldTypeNotes
items reqobject[]
Errors — 2 problem codes across 3 statuses
StatusCodetype
503admin_disabledhttps://visiblebrand.ai/problems/admin-disabled
401admin_token_invalidhttps://visiblebrand.ai/problems/admin-token-invalid

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/verification/kyb/request#

Request T2 brand verification

Enters the manual KYB review queue. T2 stacks on T1, so a workspace below t1 answers 409 kyb_requires_t1.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfKybController.request

Request

curl -sS -X POST "$VB_API/api/v1/verification/kyb/request" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 204 No Content

Queued for review.

Errors — 9 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409kyb_requires_t1https://visiblebrand.ai/problems/kyb-requires-t1
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/verification/start#

Start a domain claim

Issues a DNS-TXT or file probe token for the workspace's domain. Owner-only: it mutates the trust surface.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfVerificationController.start

Request

curl -sS -X POST "$VB_API/api/v1/verification/start" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

StartVerificationRequest

FieldTypeNotes
domain reqstringmin length 1 · max length 260
method req"dns" | "file"

Unknown properties are stripped rather than rejected.

Response 200 OK

The probe to install.

StartVerificationResponse

FieldTypeNotes
token reqstring
record reqstring
instructions reqBilingualText
Errors — 11 problem codes across 6 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
422workspace_domain_missinghttps://visiblebrand.ai/problems/workspace-domain-missing
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Verify-it-worked

The J12 loop: a done ticket that blamed specific prompts gets a baseline at done-time, a scoped re-run of exactly those prompts after the window, and a statistical verdict — moved, no_change, worsened, or an honest inconclusive. Same-regime comparisons only (AC-17-02).

GET/tickets/{id}/verification#

Verification state (verify-it-worked)

The J12 loop's two halves for one ticket: the plan written when the ticket was marked done (blocked prompts, window, baseline band — or the honest 'no_baseline'), and the re-run verdict once the window has produced one. Both are null until their moment arrives; nothing here is ever a naked number (DECISION-03-04).

role >= viewerX-Workspace-Id requiredcookie vb_sessionVerificationLoopController.getVerification

Request

curl -sS -X GET "$VB_API/api/v1/tickets/{id}/verification" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The ticket's verification plan and result, each null until written.

TicketVerificationState

FieldTypeNotes
ticketId reqstring(uuid)
verification reqobject | null
result reqobject | null
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Site changes

The staging-first write pipeline: propose → diff → gate → approve → execute → verify, with a byte-identical rollback always available.

POST/site-changes#

Propose a site change

Applies the change to STAGING, writes a diff artifact and opens a site.write gate whose meta binds tool=`cms.execute_change` plus the args-hash of exactly this change (spec 04 §6). Nothing touches production here. The entitlement check runs BEFORE the pipeline: a trial or manual-assist workspace is refused with 403 and no gate, no diff and no staged change are created.

`changeId` is a client idempotency key; a replay answers the existing change with `replayed: true`.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfSiteChangesController.propose

Request

curl -sS -X POST "$VB_API/api/v1/site-changes" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

ProposeSiteChangeRequest

FieldTypeNotes
ticketId reqstring(uuid)
changeIdstringmin length 1 · max length 200
changeType req"meta_update" | "schema_inject" | "robots_update" | "content_publish"
targetUrl reqstring(uri)
payloadmap
summarystringmin length 1

Unknown properties are stripped rather than rejected.

Response 201 Created

The staged change and its gate.

ProposeSiteChangeResponse

FieldTypeNotes
change reqSiteChange
gateId reqstring(uuid)
replayed reqboolean
Errors — 13 problem codes across 7 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403entitlement_writes_refusedhttps://visiblebrand.ai/problems/entitlement-writes-refused
422invariant_violationhttps://visiblebrand.ai/problems/invariant-violation
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
502staging_failedhttps://visiblebrand.ai/problems/staging-failed
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/site-changes/{id}#

Site change status

Status, diff card data, verification outcome and whether a rollback token is held.

role >= viewerX-Workspace-Id requiredcookie vb_sessionSiteChangesController.byId

Request

curl -sS -X GET "$VB_API/api/v1/site-changes/{id}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The change.

SiteChange

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
changeId reqstring
changeType reqstring
targetUrl reqstring
status reqenum(6)none, applied, verified, verification_failed, committed, rolled_back
gateId reqstring(uuid)
diffArtifactPath reqstring
diff reqStagingDiff
hasRollbackToken reqboolean
verificationobject
createdAt reqstring(date-time)
appliedAtstring(date-time)
rolledBackAtstring(date-time)
Errors — 7 problem codes across 5 statuses
StatusCodetype
404change_not_foundhttps://visiblebrand.ai/problems/change-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/site-changes/{id}/execute#

Manual execute retry (owner)

NOT the primary path — approving the gate auto-executes the bound change. This exists for retries after a crash or outage: it re-checks that the gate is human-approved and the tier is sufficient, then mints a FRESH single-use token server-side. No token ever crosses HTTP.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfSiteChangesController.execute

Request

curl -sS -X POST "$VB_API/api/v1/site-changes/{id}/execute" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The executed change.

SiteChange

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
changeId reqstring
changeType reqstring
targetUrl reqstring
status reqenum(6)none, applied, verified, verification_failed, committed, rolled_back
gateId reqstring(uuid)
diffArtifactPath reqstring
diff reqStagingDiff
hasRollbackToken reqboolean
verificationobject
createdAt reqstring(date-time)
appliedAtstring(date-time)
rolledBackAtstring(date-time)
Errors — 16 problem codes across 7 statuses
StatusCodetype
403args_hash_mismatchhttps://visiblebrand.ai/problems/args-hash-mismatch
404change_not_foundhttps://visiblebrand.ai/problems/change-not-found
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
502execution_failedhttps://visiblebrand.ai/problems/execution-failed
403execution_refusedhttps://visiblebrand.ai/problems/execution-refused
409gate_not_approvedhttps://visiblebrand.ai/problems/gate-not-approved
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
502rollback_failedhttps://visiblebrand.ai/problems/rollback-failed
403tier_requirementhttps://visiblebrand.ai/problems/tier-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
502verification_failedhttps://visiblebrand.ai/problems/verification-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/site-changes/{id}/rollback#

Roll back a site change

Restores the byte-identical prior state from the stored snapshot (AC-02-04). A failed rollback keeps the token durable so a manual retry stays possible.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfSiteChangesController.rollback

Request

curl -sS -X POST "$VB_API/api/v1/site-changes/{id}/rollback" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The rolled-back change.

SiteChange

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
changeId reqstring
changeType reqstring
targetUrl reqstring
status reqenum(6)none, applied, verified, verification_failed, committed, rolled_back
gateId reqstring(uuid)
diffArtifactPath reqstring
diff reqStagingDiff
hasRollbackToken reqboolean
verificationobject
createdAt reqstring(date-time)
appliedAtstring(date-time)
rolledBackAtstring(date-time)
Errors — 11 problem codes across 7 statuses
StatusCodetype
404change_not_foundhttps://visiblebrand.ai/problems/change-not-found
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409invalid_transitionhttps://visiblebrand.ai/problems/invalid-transition
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
502rollback_failedhttps://visiblebrand.ai/problems/rollback-failed
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Content publish

Draft submission through the deterministic policy engine. A failing verdict is a 422 carrying every violation with its exact excerpt, and NO gate is created (DECISION-24-01).

POST/content/drafts#

Submit a content draft

The deterministic policy engine runs FIRST. On a failing verdict the draft persists as `policy_failed` and the response is a 422 problem+json carrying every violation WITH its exact excerpt — the rewrite-loop feed — and NO gate exists (DECISION-24-01). On a passing verdict: staging preview, diff and a content.publish gate bound to tool + args-hash. The entitlement check precedes all of it.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfContentController.submit

Request

curl -sS -X POST "$VB_API/api/v1/content/drafts" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

SubmitContentDraftRequest

FieldTypeNotes
ticketId reqstring(uuid)
vertical reqenum(6)health, finance, insurance, legal, food, general
destination req"client_site" | "network_property" | "gbp" | "social_draft"
countrystringpattern ^[A-Za-z]{2,3}$
draft reqobject
slugstringmin length 1 · max length 200
lexiconobject
corpusTextsstring[]

Unknown properties are stripped rather than rejected.

Response 201 Created

The gated draft and its gate.

SubmitContentDraftResponse

FieldTypeNotes
draft reqContentDraft
gateId reqstring(uuid)
Errors — 14 problem codes across 7 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
422destination_unsupportedhttps://visiblebrand.ai/problems/destination-unsupported
403entitlement_writes_refusedhttps://visiblebrand.ai/problems/entitlement-writes-refused
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
422policy_failedhttps://visiblebrand.ai/problems/policy-failed
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
502staging_failedhttps://visiblebrand.ai/problems/staging-failed
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/content/drafts/{id}#

Draft status

Status, policy verdict, per-locale claim ledgers and the staging preview URL.

role >= viewerX-Workspace-Id requiredcookie vb_sessionContentController.byId

Request

curl -sS -X GET "$VB_API/api/v1/content/drafts/{id}" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The draft.

ContentDraft

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
vertical reqstring
destination reqstring
countrystring
locales reqmap
status reqenum(5)draft, policy_failed, gated, published, publish_failed
policyVerdictmap
gateIdstring(uuid)
changeIdstring
targetUrlstring
diffStagingDiff
diffArtifactPathstring
previewUrlstring
hasRollbackToken reqboolean
verificationobject
publishedUrlstring
publishedAtstring(date-time)
createdAt reqstring(date-time)
updatedAt reqstring(date-time)
Errors — 7 problem codes across 5 statuses
StatusCodetype
404draft_not_foundhttps://visiblebrand.ai/problems/draft-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/content/drafts/{id}/republish#

Manual republish retry (owner)

The retry twin of the site-change manual execute: re-checks the human approval and tier, then mints a fresh single-use token server-side. A gated draft without a stored passing verdict is refused (409 policy_pass_required) rather than published.

role >= ownerX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfContentController.republish

Request

curl -sS -X POST "$VB_API/api/v1/content/drafts/{id}/republish" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The republished draft.

ContentDraft

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
vertical reqstring
destination reqstring
countrystring
locales reqmap
status reqenum(5)draft, policy_failed, gated, published, publish_failed
policyVerdictmap
gateIdstring(uuid)
changeIdstring
targetUrlstring
diffStagingDiff
diffArtifactPathstring
previewUrlstring
hasRollbackToken reqboolean
verificationobject
publishedUrlstring
publishedAtstring(date-time)
createdAt reqstring(date-time)
updatedAt reqstring(date-time)
Errors — 16 problem codes across 7 statuses
StatusCodetype
403args_hash_mismatchhttps://visiblebrand.ai/problems/args-hash-mismatch
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
404draft_not_foundhttps://visiblebrand.ai/problems/draft-not-found
502execution_failedhttps://visiblebrand.ai/problems/execution-failed
403execution_refusedhttps://visiblebrand.ai/problems/execution-refused
409gate_not_approvedhttps://visiblebrand.ai/problems/gate-not-approved
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
409policy_pass_requiredhttps://visiblebrand.ai/problems/policy-pass-required
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
403tier_requirementhttps://visiblebrand.ai/problems/tier-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
502verification_failedhttps://visiblebrand.ai/problems/verification-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Chat & onboarding

The durable agent conversation: transcript, send, session list and the SSE stream that replays then follows live.

GET/chat/{chatSessionId}/messages#

The durable transcript

The source of truth for a conversation — how a reload or a brand-new browser rebuilds it. Never depends on the live ring.

role >= viewerX-Workspace-Id requiredcookie vb_sessionChatController.messages

Request

curl -sS -X GET "$VB_API/api/v1/chat/{chatSessionId}/messages" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
chatSessionId reqpathstringChat session uuid.
afterSeqqueryintegerResume cursor — return only messages with a higher chat_messages.seq. Absent replays the whole transcript (seq 0 is a real message, so absence and 0 differ).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The transcript from the cursor onward.

ChatMessagesResponse

FieldTypeNotes
items reqChatMessage[]
stepsobject[]
Errors — 8 problem codes across 5 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/chat/{chatSessionId}/messages#

Send a turn

202, not 200: the message is durably recorded and a job is queued, but the AGENT'S reply is produced asynchronously by the runtime and arrives over the SSE stream. Claiming 200/complete would be a lie.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfChatController.send

Request

curl -sS -X POST "$VB_API/api/v1/chat/{chatSessionId}/messages" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
chatSessionId reqpathstringChat session uuid.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

SendChatMessageRequest

FieldTypeNotes
text reqstringmin length 1 · max length 4000

Unknown properties are stripped rather than rejected.

Response 202 Accepted

Accepted; the durable message id and seq.

SendChatMessageResponse

FieldTypeNotes
messageId reqstring(uuid)
seq reqinteger>= 0 · <= 9007199254740991
Errors — 10 problem codes across 5 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/chat/{chatSessionId}/stream#

Chat stream (SSE)

Replays the durable transcript from the resume cursor, then follows live — gap-free: a buffering listener attaches synchronously so frames produced during the transcript read are captured, then deduped by durable seq. `id:` carries chat_messages.seq, and frames with no durable row (mid-turn deltas, tool lines) carry NO `id:`, so Last-Event-ID never advances past something the transcript could not replay. The browser's automatic Last-Event-ID header wins over ?afterSeq=.

role >= viewerX-Workspace-Id requiredcookie vb_sessionChatController.stream

Request

curl -sS -X GET "$VB_API/api/v1/chat/{chatSessionId}/stream" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -N -H "Accept: text/event-stream"

Parameters

NameInTypeDescription
chatSessionId reqpathstringChat session uuid.
afterSeqqueryintegerResume cursor — return only messages with a higher chat_messages.seq. Absent replays the whole transcript (seq 0 is a real message, so absence and 0 differ).
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.
Last-Event-IDheaderstringSet automatically by EventSource on reconnect. A malformed value degrades to ?afterSeq=, then to a full replay.

Response 200 OK

An open event stream whose `data:` payloads are ChatStreamEvent objects. The schema describes ONE `data:` payload, not the whole body.

ChatStreamEvent

One of 10 variants:

session.started
FieldTypeNotes
t req"session.started"
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
thought.summary
FieldTypeNotes
t req"thought.summary"
text reqstring
tool.call
FieldTypeNotes
t req"tool.call"
name reqstringmin length 1
summary reqstring
activityBilingualText
tool.result
FieldTypeNotes
t req"tool.result"
name reqstringmin length 1
summary reqstring
activityBilingualText
text.delta
FieldTypeNotes
t req"text.delta"
text reqstring
message_seqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
message_offsetinteger>= 0 · <= 9007199254740991
message.closed
FieldTypeNotes
t req"message.closed"
message_seq reqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
artifact.created
FieldTypeNotes
t req"artifact.created"
kind reqstringmin length 1
path reqstringmin length 1
preview_urlstring(uri)
gate.requested
FieldTypeNotes
t req"gate.requested"
gate_id reqstring(uuid)
session.ended
FieldTypeNotes
t req"session.ended"
result req"done" | "blocked" | "failed"
message.card
FieldTypeNotes
t req"message.card"
card reqobject | object | object
Errors — 8 problem codes across 5 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/chat/sessions#

List conversations

The workspace's chat sessions, newest first. No route ambiguity with the parameterized chat paths: `sessions` is not a uuid.

role >= viewerX-Workspace-Id requiredcookie vb_sessionChatController.sessions

Request

curl -sS -X GET "$VB_API/api/v1/chat/sessions" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The session list.

Errors — 6 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/workspaces/onboarding/start#

Start an onboarding conversation

Opens a chat session and persists a context-aware bilingual opening that confirms what is already on file and asks only for what is missing.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfChatController.start

Request

curl -sS -X POST "$VB_API/api/v1/workspaces/onboarding/start" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

The new chat session id.

StartOnboardingResponse

FieldTypeNotes
chatSessionId reqstring(uuid)
Errors — 8 problem codes across 5 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Realtime

Per-ticket agent activity as Server-Sent Events. Curated frames only, never raw chain-of-thought (DECISION-06-02).

GET/events/history#

Workspace event history

The REST read over the durable workspace event log the WS board channel replays (spec 04 §5): every retained board-channel event, oldest first, keyset paged on the opaque event_id. The log is a retained window (in-memory ring in dev; Redis Streams MAXLEN in production), not an archive — page one simply starts at the oldest event still retained. Per-connection presence hello frames never appear: they are snapshots, not history.

`head` is the log's current head event_id — the same baseline the WS hello frame carries, so a client can read history here and then resume live on the board channel without a gap.

role >= viewerX-Workspace-Id requiredcookie vb_sessionEventHistoryController.history

Request

curl -sS -X GET "$VB_API/api/v1/events/history" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE"

Parameters

NameInTypeDescription
afterquerystringKeyset cursor: the event_id of the last event from the previous page (the previous response's nextCursor). Absent (or '0') starts at the oldest retained event.
limitqueryintegerPage size, 1–500. Defaults to the server page size.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

A page of events. `at` is the append instant when the log recorded one (Redis Streams entry ids carry it); null from the dev in-memory ring, which records order only — clients must not invent times for those.

EventHistoryPage

FieldTypeNotes
items reqobject[]
nextCursor reqstring | null
head reqstring
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/tickets/{id}/stream#

Ticket activity stream (SSE)

Server-Sent Events of the agent working this ticket: replays the recent ring, then follows live until the client disconnects. `id:` carries the ring seq. Heartbeat comment every 15s. Curated frames only — never raw chain-of-thought (DECISION-06-02).

Workspace scope is asserted before any stream bytes are written.

role >= viewerX-Workspace-Id requiredcookie vb_sessionTicketStreamController.stream

Request

curl -sS -X GET "$VB_API/api/v1/tickets/{id}/stream" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -N -H "Accept: text/event-stream"

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 200 OK

An open event stream whose `data:` payloads are TicketStreamEvent objects. The schema describes ONE `data:` payload, not the whole body.

TicketStreamEvent

One of 9 variants:

session.started
FieldTypeNotes
t req"session.started"
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
thought.summary
FieldTypeNotes
t req"thought.summary"
text reqstring
tool.call
FieldTypeNotes
t req"tool.call"
name reqstringmin length 1
summary reqstring
activityBilingualText
tool.result
FieldTypeNotes
t req"tool.result"
name reqstringmin length 1
summary reqstring
activityBilingualText
text.delta
FieldTypeNotes
t req"text.delta"
text reqstring
message_seqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
message_offsetinteger>= 0 · <= 9007199254740991
message.closed
FieldTypeNotes
t req"message.closed"
message_seq reqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
artifact.created
FieldTypeNotes
t req"artifact.created"
kind reqstringmin length 1
path reqstringmin length 1
preview_urlstring(uri)
gate.requested
FieldTypeNotes
t req"gate.requested"
gate_id reqstring(uuid)
session.ended
FieldTypeNotes
t req"session.ended"
result req"done" | "blocked" | "failed"
Errors — 7 problem codes across 5 statuses
StatusCodetype
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Run triggers

In-product triggers that enqueue a measurement cycle or a technical audit.

POST/audits#

Start a technical audit

Enqueues one audit cycle against the workspace's primary_domain. NO BODY on purpose: the URL is never caller-chosen, so this route cannot be turned into a crawler for arbitrary sites. 422 when the workspace has no domain on file.

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfAuditTriggersController.start

Request

curl -sS -X POST "$VB_API/api/v1/audits" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF"

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Response 202 Accepted

Accepted; the enqueued audit handle.

StartAuditResponse

FieldTypeNotes
enqueued req"true"
queue req"vb:audit"
jobId reqstringmin length 1
baseUrl reqstring(uri)
note reqstringmin length 1
Errors — 11 problem codes across 8 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
503queue_unavailablehttps://visiblebrand.ai/problems/queue-unavailable
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
501run_trigger_requires_databasehttps://visiblebrand.ai/problems/run-trigger-requires-database
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
422workspace_domain_missinghttps://visiblebrand.ai/problems/workspace-domain-missing
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/runs#

Start a measurement run

Enqueues one full measurement cycle. 202 — the work happens on the BullMQ fleet, so claiming a completed run would be a lie. Requires a FROZEN bank: a run against a draft bank would not be comparable (409 no_frozen_bank).

role >= approverX-Workspace-Id requiredCSRF token requiredcookie vb_sessionheader X-VB-CsrfRunTriggersController.start

Request

curl -sS -X POST "$VB_API/api/v1/runs" \
  -b "vb_session=$VB_SESSION" \
  -H "X-Workspace-Id: $VB_WORKSPACE" \
  -b "vb_csrf=$VB_CSRF" -H "X-VB-Csrf: $VB_CSRF" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
X-Workspace-Id reqheaderstring(uuid)The workspace this request acts inside. Required on every workspace-scoped route: the guard rejects a missing (400 workspace_header_missing), malformed (400 workspace_header_invalid) or unknown (404 workspace_not_found) value before any handler runs, and the value is pinned into the Postgres session so row-level security scopes every query behind it. Obtain one from GET /workspaces.

Request body (application/json)

StartRunRequest

FieldTypeNotes
locale"ar" | "en"default "en"
runnerModeenum(5)api_model, api_model_websearch, consumer_surface, direct_api, fal_llm · default "api_model"

Unknown properties are stripped rather than rejected.

Response 202 Accepted

Accepted; the enqueued run handle.

StartRunResponse

FieldTypeNotes
enqueued req"true"
queue req"vb:run-bank"
jobId reqstringmin length 1
bankId reqstring(uuid)
bankVersion reqinteger<= 9007199254740991
locale req"ar" | "en"
samples reqinteger>= 1 · <= 10
runnerMode reqenum(5)api_model, api_model_websearch, consumer_surface, direct_api, fal_llm
note reqstringmin length 1
Errors — 12 problem codes across 8 statuses
StatusCodetype
403csrf_token_invalidhttps://visiblebrand.ai/problems/csrf-token-invalid
403csrf_token_missinghttps://visiblebrand.ai/problems/csrf-token-missing
409no_frozen_bankhttps://visiblebrand.ai/problems/no-frozen-bank
403not_a_memberhttps://visiblebrand.ai/problems/not-a-member
503queue_unavailablehttps://visiblebrand.ai/problems/queue-unavailable
403role_requirementhttps://visiblebrand.ai/problems/role-requirement
501run_trigger_requires_databasehttps://visiblebrand.ai/problems/run-trigger-requires-database
401unauthorizedhttps://visiblebrand.ai/problems/unauthorized
400validation_failedhttps://visiblebrand.ai/problems/validation-failed
400workspace_header_invalidhttps://visiblebrand.ai/problems/workspace-header-invalid
400workspace_header_missinghttps://visiblebrand.ai/problems/workspace-header-missing
404workspace_not_foundhttps://visiblebrand.ai/problems/workspace-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

Health

Unauthenticated liveness probe.

GET/health#

Liveness probe

The only unauthenticated, unscoped route in the API. Answers the process status and the api version that produced this document.

no workspace rolenot workspace-scopedunauthenticatedHealthController.health

Request

curl -sS -X GET "$VB_API/api/v1/health"

Response 200 OK

The process is up.

HealthResponse

FieldTypeNotes
status req"ok"
version reqstring
Errors — 0 problem codes across 1 status
StatusCodetype

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/health/ready#

Readiness probe

Can THIS process reach its own database and Redis? Liveness cannot answer that, and an api serving /health while Postgres is unreachable is the classic false green. ALWAYS answers 200, even when degraded — a non-2xx here would make a naive monitor or load balancer pull a degraded-but-serving instance out of rotation, turning a partial failure into a total one. The verdict is in the body, per dependency.

Sends `cache-control: no-store`. Unauthenticated, like /health.

no workspace rolenot workspace-scopedunauthenticatedHealthController.ready

Request

curl -sS -X GET "$VB_API/api/v1/health/ready"

Response 200 OK

The verdict plus one row per dependency. `status` is `ok` or `degraded`.

ReadinessResponse

FieldTypeNotes
status req"ok" | "degraded"
version reqstring
dependencies reqobject[]
Errors — 0 problem codes across 1 status
StatusCodetype

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/health/status#

Public status page

The whole status document in one response: overall state with the reason it was reached, per-component history, and open incidents. Unauthenticated and unscoped on purpose — a status page that requires a login is worthless during the outage that broke logins. Safe because the data is platform-scoped by construction: no query takes a workspace, the tables carry no workspace_id, and every probe detail string is sanitised before it is stored.

Sends `cache-control: no-store` rather than a short cache: a cached status page reports the state of the world as it was without saying so.

no workspace rolenot workspace-scopedunauthenticatedHealthController.statusPage

Request

curl -sS -X GET "$VB_API/api/v1/health/status"

Response 200 OK

Components, incidents, and an honest `source` field saying whether this was read live from the probe store or served degraded.

StatusPage

FieldTypeNotes
generatedAt reqstring(date-time)
overall req"operational" | "degraded" | "outage" | "unknown"
overallReason reqenum(6)all_operational, core_outage, core_degraded, vendor_degraded, data_stale, no_data
probeIntervalMs reqinteger<= 9007199254740991
staleAfterMs reqinteger<= 9007199254740991
historyDays reqinteger<= 9007199254740991
components reqobject[]
incidents reqobject[]
source req"live" | "degraded"
Errors — 0 problem codes across 1 status
StatusCodetype

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/health/status/incidents/{id}/updates#

Append an incident update (platform admin)

Adds a human-written, bilingual update to an incident. ADDITIVE ONLY: it cannot edit or delete what the prober observed — an operator adds context, they do not get to rewrite measurements. Authority is the existing x-vb-admin-token shared secret (the KYB-queue precedent), not a new scheme. An api with no status store at all answers 501 rather than pretending to have recorded it.

no workspace rolenot workspace-scopedheader x-vb-admin-tokenHealthController.appendIncidentUpdate

Request

curl -sS -X POST "$VB_API/api/v1/health/status/incidents/{id}/updates" \
  -H "x-vb-admin-token: $VB_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
id reqpathstringThe resource id. A uuid on every route except the site-change and content-draft routes, where it may also be the client-chosen change id.

Request body (application/json)

StatusIncidentUpdateRequest

FieldTypeNotes
body reqBilingualTextInput

Unknown properties are stripped rather than rejected.

Response 201 Created

The update was appended.

IncidentUpdateAck

FieldTypeNotes
ok req"true"
Errors — 5 problem codes across 6 statuses
StatusCodetype
503admin_disabledhttps://visiblebrand.ai/problems/admin-disabled
401admin_token_invalidhttps://visiblebrand.ai/problems/admin-token-invalid
404not_foundhttps://visiblebrand.ai/problems/not-found
501not_implementedhttps://visiblebrand.ai/problems/not-implemented
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

Internal — event ingest internal only

Service-to-service only. Authenticated by the shared X-VB-Ingest-Token secret, never by a session; the payload carries the workspace and every handler re-checks resource scope.

POST/internal/ingest/chat-events#

Ingest chat stream events

INTERNAL. Write-back, not just fan-out: agent output is PERSISTED as chat_messages rows before it is streamed, so an agent reply survives a restart exactly like the human turn that prompted it. Every event is re-validated against ChatStreamEvent BEFORE any write, and the whole batch is rejected with the index of the first invalid event, so a bad batch cannot half-apply to a transcript.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenIngestController.ingestChatEvents

Request

curl -sS -X POST "$VB_API/api/v1/internal/ingest/chat-events" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

ChatEventIngestRequest

FieldTypeNotes
workspaceId reqstring(uuid)
chatSessionId reqstring(uuid)
turnIdstringmin length 1 · max length 128
events reqmap[]min 1 items · max 200 items

Unknown properties are stripped rather than rejected.

Response 200 OK

How many events were accepted.

TicketEventIngestResponse

FieldTypeNotes
accepted reqinteger>= 0 · <= 9007199254740991
Errors — 5 problem codes across 6 statuses
StatusCodetype
422chat_event_invalidhttps://visiblebrand.ai/problems/chat-event-invalid
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/internal/ingest/ticket-events#

Ingest ticket stream events

INTERNAL. The bridge the agent-runtime posts through so its session events surface on the ticket SSE stream. The dev/v0 transport for the Redis Streams pipeline — same contract-shaped events, so swapping the transport never changes payloads. The ticket must exist in the workspace the payload claims; unknown and cross-workspace both 404.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenIngestController.ingestTicketEvents

Request

curl -sS -X POST "$VB_API/api/v1/internal/ingest/ticket-events" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Request body (application/json)

TicketEventIngestRequest

FieldTypeNotes
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
events requnion(9)[]min 1 items · max 200 items

Unknown properties are stripped rather than rejected.

Response 200 OK

How many events were accepted.

TicketEventIngestResponse

FieldTypeNotes
accepted reqinteger>= 0 · <= 9007199254740991
Errors — 4 problem codes across 5 statuses
StatusCodetype
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

Internal — agent runtime internal only

Service-to-service only. The agent-runtime reads transcripts and writes profile DRAFTS here; scope rides the path and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.

GET/internal/agent/workspaces/{workspaceId}/chat/{chatSessionId}/messages#

Read a transcript (internal)

INTERNAL. The runtime reads the user's turn from the durable transcript (its queue job carries ids only). Scope comes from the PATH and is resource-checked: unknown and cross-workspace sessions answer the identical 404, so there is no existence oracle.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.messages

Request

curl -sS -X GET "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/chat/{chatSessionId}/messages" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN"

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.
chatSessionId reqpathstringChat session uuid.
afterSeqqueryintegerResume cursor — return only messages with a higher chat_messages.seq. Absent replays the whole transcript (seq 0 is a real message, so absence and 0 differ).

Response 200 OK

The transcript.

ChatMessagesResponse

FieldTypeNotes
items reqChatMessage[]
stepsobject[]
Errors — 4 problem codes across 5 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/internal/agent/workspaces/{workspaceId}/chat/{chatSessionId}/session-meta#

Read the SDK resume pointer (internal)

INTERNAL. The stored Claude Agent SDK session id. This is what lets a turn CONTINUE the model's own session instead of starting a stranger — the fix for the production bug where a user typed "approve" and the agent re-crawled and re-presented.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.sessionMeta

Request

curl -sS -X GET "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/chat/{chatSessionId}/session-meta" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN"

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.
chatSessionId reqpathstringChat session uuid.

Response 200 OK

The session meta.

SessionMetaResponse

FieldTypeNotes
chatSessionId reqstring(uuid)
agentKey reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
status reqstring
agentSessionId reqstring | null
Errors — 3 problem codes across 4 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid

Every error body is application/problem+json. Switch on type, never on title or detail.

PATCH/internal/agent/workspaces/{workspaceId}/chat/{chatSessionId}/session-meta#

Store the SDK resume pointer (internal)

INTERNAL. Persists the SDK-assigned session id after a turn. Last write wins: a resume-miss fallback overwrites the stale pointer so the NEXT turn resumes the session that actually ran.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.patchSessionMeta

Request

curl -sS -X PATCH "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/chat/{chatSessionId}/session-meta" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.
chatSessionId reqpathstringChat session uuid.

Request body (application/json)

PatchSessionMetaRequest

FieldTypeNotes
agentSessionId reqstringmin length 1 · max length 256

Unknown properties are stripped rather than rejected.

Response 200 OK

The stored session meta.

SessionMetaResponse

FieldTypeNotes
chatSessionId reqstring(uuid)
agentKey reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
status reqstring
agentSessionId reqstring | null
Errors — 4 problem codes across 5 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/internal/agent/workspaces/{workspaceId}/costs#

Meter chat-turn model spend (internal)

INTERNAL. One row per (model, session) usage entry of a turn: token counts are exact, costUsd is the SDK's per-model estimate. `chatSessionId` is REQUIRED as the scope proof — every named session must resolve inside the path workspace before anything is written, otherwise a caller could meter spend into any workspace uuid it invents. Real mode only; metering mock turns would fabricate spend.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.recordCosts

Request

curl -sS -X POST "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/costs" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.

Request body (application/json)

RecordCostsRequest

FieldTypeNotes
rows reqobject[]min 1 items · max 50 items

Unknown properties are stripped rather than rejected.

Response 201 Created

How many rows were recorded.

RecordCostsResponse

FieldTypeNotes
recorded reqinteger>= 0 · <= 9007199254740991
Errors — 4 problem codes across 5 statuses
StatusCodetype
404chat_session_not_foundhttps://visiblebrand.ai/problems/chat-session-not-found
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/internal/agent/workspaces/{workspaceId}/profile#

Read the profile (internal)

INTERNAL. The same DTO the public GET /profile serves, primaryDomain included, so the two surfaces cannot disagree about what is on file.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.getProfile

Request

curl -sS -X GET "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/profile" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN"

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.

Response 200 OK

The profile.

BusinessProfile

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
name reqstringmin length 1
primaryDomainstringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offerings reqobject[]
serviceAreas reqobject[]
languages reqstring[]
personas reqobject[]
socials reqobject[]
brandAliases reqstring[]
competitors reqobject[]default []
techStackobject
fieldProvenancemap
status req"draft" | "approved"
approvedBystring(uuid)
approvedAtstring(date-time)
Errors — 3 problem codes across 4 statuses
StatusCodetype
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
404profile_not_foundhttps://visiblebrand.ai/problems/profile-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

PATCH/internal/agent/workspaces/{workspaceId}/profile#

Write a profile draft (internal)

INTERNAL. The runtime writes the DRAFT a human then approves — draft-only by construction (an approved profile answers 409), so this is NOT a Gatekeeper bypass: a draft's only route to `approved` remains a human clicking approve.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.patchProfile

Request

curl -sS -X PATCH "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/profile" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.

Request body (application/json)

PatchProfileRequest

FieldTypeNotes
namestringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offeringsobject[]
serviceAreasobject[]
languagesstring[]
personasobject[]
socialsobject[]
brandAliasesstring[]
competitorsobject[]
techStackobject
fieldProvenancemap

Unknown properties are stripped rather than rejected.

Response 200 OK

The updated draft.

BusinessProfile

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
name reqstringmin length 1
primaryDomainstringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offerings reqobject[]
serviceAreas reqobject[]
languages reqstring[]
personas reqobject[]
socials reqobject[]
brandAliases reqstring[]
competitors reqobject[]default []
techStackobject
fieldProvenancemap
status req"draft" | "approved"
approvedBystring(uuid)
approvedAtstring(date-time)
Errors — 5 problem codes across 6 statuses
StatusCodetype
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
409profile_approved_immutablehttps://visiblebrand.ai/problems/profile-approved-immutable
404profile_not_foundhttps://visiblebrand.ai/problems/profile-not-found
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/internal/agent/workspaces/{workspaceId}/site-changes#

Propose a site change (internal)

INTERNAL. PROPOSE only: staging apply + diff artifact + the site.write gate bound to this exact args-hash (spec 02 §4 steps 2-3). NOT a write — production is touched only after a human approves and the Gatekeeper mints a single-use token server-side (DECISION-04-06), and nothing on this route can shorten that path. It reuses the SAME SiteChangesService.propose the public route calls, and the AC-15-02 / DECISION-15-02 entitlement check runs FIRST: a trial or manual-assist workspace is refused before anything is staged. Being service-authenticated says who is calling, not what the plan bought.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.proposeSiteChange

Request

curl -sS -X POST "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/site-changes" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.

Request body (application/json)

AgentProposeSiteChangeRequest

FieldTypeNotes
ticketId reqstring(uuid)
changeIdstring
changeType reqstring
targetUrl reqstring
payload reqmap
summarystring

Unknown properties are stripped rather than rejected.

Response 200 OK

The staged change + gate.

ProposeSiteChangeResponse

FieldTypeNotes
change reqSiteChange
gateId reqstring(uuid)
replayed reqboolean
Errors — 5 problem codes across 6 statuses
StatusCodetype
403entitlement_writes_refusedhttps://visiblebrand.ai/problems/entitlement-writes-refused
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

GET/internal/agent/workspaces/{workspaceId}/tickets/{ticketId}#

Read a ticket work order (internal)

INTERNAL. The read half of the vb:agent-ticket seam — that queue job carries ids only, deliberately, so a re-delivery can never replay a superseded description. Answers the ticket plus, inside its opaque payload, the findings it was opened from (DECISION-17-03 makes those the agent's mandate: no finding, no work), the scan window that produced them, and the business profile including the target domain. It cannot be the PUBLIC GET /tickets/{id}: that route needs a user identity the runtime has none of and must not impersonate. Read-only by construction — nothing here moves the ticket, comments on it or touches a gate. Unknown and cross-workspace tickets answer the identical 404.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.ticketView

Request

curl -sS -X GET "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/tickets/{ticketId}" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN"

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.
ticketId reqpathstringticketId path parameter.

Response 200 OK

The ticket and its work context.

AgentTicketView

FieldTypeNotes
id reqstring
title reqstring
status reqstring
agentKeystring
severitystring
payload reqmap
targetUrlstring
Errors — 3 problem codes across 4 statuses
StatusCodetype
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found

Every error body is application/problem+json. Switch on type, never on title or detail.

POST/internal/agent/workspaces/{workspaceId}/tickets/{ticketId}/comments#

Comment as an agent (internal)

INTERNAL. The agent's structured progress / blocker comment (spec 02 §0). The public POST /tickets/{id}/comments stamps the CurrentUser as a HUMAN author by design, which would put a person's byline on machine work in the audit trail and on the card — so the author here is the AGENT. Mentions are still extracted deterministically server-side; nothing the agent writes chooses its own author kind or forges a mention list.

no workspace rolenot workspace-scopedheader x-vb-ingest-tokenInternalAgentController.postComment

Request

curl -sS -X POST "$VB_API/api/v1/internal/agent/workspaces/{workspaceId}/tickets/{ticketId}/comments" \
  -H "x-vb-ingest-token: $VB_INGEST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ … }'

Parameters

NameInTypeDescription
workspaceId reqpathstringWorkspace uuid. On the internal surfaces the scope rides the PATH (never a caller-claimed identity) and is resource-checked, so a cross-workspace call 404s exactly like an unknown one.
ticketId reqpathstringticketId path parameter.

Request body (application/json)

AgentCommentRequest

FieldTypeNotes
agentKey reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
bodyMd reqstring

Unknown properties are stripped rather than rejected.

Response 200 OK

The stored comment id.

AgentCommentResponse

FieldTypeNotes
id reqstring(uuid)
Errors — 4 problem codes across 5 statuses
StatusCodetype
503ingest_disabledhttps://visiblebrand.ai/problems/ingest-disabled
401ingest_token_invalidhttps://visiblebrand.ai/problems/ingest-token-invalid
404ticket_not_foundhttps://visiblebrand.ai/problems/ticket-not-found
400validation_failedhttps://visiblebrand.ai/problems/validation-failed

Every error body is application/problem+json. Switch on type, never on title or detail.

Schemas

122 named components. Unmarked schemas are the zod objects in @vb/contracts that the server validates with; the mirror ones are described from a shape that still lives in apps/api (see Stability). Names ending in Input are the request-direction conversion of a schema that also appears in responses: a zod object strips unknown keys on parse, so the two directions are genuinely not the same shape.

AgentCommentRequest mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
agentKey reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
bodyMd reqstring

Unknown properties are stripped rather than rejected.

AgentCommentResponse mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
id reqstring(uuid)
AgentProposeSiteChangeRequest mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
ticketId reqstring(uuid)
changeIdstring
changeType reqstring
targetUrl reqstring
payload reqmap
summarystring

Unknown properties are stripped rather than rejected.

AgentTicketView mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
id reqstring
title reqstring
status reqstring
agentKeystring
severitystring
payload reqmap
targetUrlstring
ApprovePlacementResponse mirror

Mirrors apps/api/src/placements/placements.service.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
gate reqGate
replayed reqboolean
AuthIdentity
FieldTypeNotes
userId reqstring(uuid)
email reqstring
name reqstring | null
locale req"ar" | "en"
issuedAt reqstring(date-time)
expiresAt reqstring(date-time)
memberships reqMembershipSummary[]
AutonomySettings
FieldTypeNotes
policies reqobject[]
BankApproveResponse mirror

Mirrors apps/api/src/measurement/prompt-banks.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
bankId reqstring(uuid)
version reqinteger<= 9007199254740991
status reqstring
measurementobject
BankPromptsResponse mirror

Mirrors apps/api/src/measurement/prompt-bank-ops.ts (BankPromptsDto). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
bankId reqstring(uuid)
version reqinteger<= 9007199254740991
status req"draft" | "approved" | "frozen" | "superseded"
locale req"ar" | "en"
groups reqobject[]
counts reqobject
coverage reqobject
BilingualText
FieldTypeNotes
en reqstring
ar reqstring
BilingualTextInput
FieldTypeNotes
en reqstring
ar reqstring

Unknown properties are stripped rather than rejected.

BoardResponse mirror

Mirrors apps/api/src/board/board.service.ts (BoardDto). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
columns reqobject[]min 8 items · max 8 items
BuildBankRequest mirror

Mirrors @vb/app BuildPromptBankInput minus workspaceId. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
locale req"ar" | "en"
planSize reqinteger<= 9007199254740991
classSharesmap
candidates reqobject[]min 1 items
brandAliases reqstring[]min 1 items
competitorAliasesstring[]

Unknown properties are stripped rather than rejected.

BuildPromptBankResponse mirror

Mirrors @vb/app BuildPromptBankResult (ok branch). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
ok req"true"
bankId reqstring(uuid)
version reqinteger<= 9007199254740991
kept reqobject[]
dropped reqobject
coverage reqobject[]
shareViolations reqobject[]
BulkPromptsRequest
FieldTypeNotes
promptIds reqstring(uuid)[]min 1 items · max 500 items
action req"enable" | "disable"

Unknown properties are stripped rather than rejected.

BulkPromptsResponse
FieldTypeNotes
bankId reqstring(uuid)
action req"enable" | "disable"
requested reqinteger<= 9007199254740991
updated reqinteger>= 0 · <= 9007199254740991
BusinessProfile
FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
name reqstringmin length 1
primaryDomainstringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offerings reqobject[]
serviceAreas reqobject[]
languages reqstring[]
personas reqobject[]
socials reqobject[]
brandAliases reqstring[]
competitors reqobject[]default []
techStackobject
fieldProvenancemap
status req"draft" | "approved"
approvedBystring(uuid)
approvedAtstring(date-time)
ChatEventIngestRequest
FieldTypeNotes
workspaceId reqstring(uuid)
chatSessionId reqstring(uuid)
turnIdstringmin length 1 · max length 128
events reqmap[]min 1 items · max 200 items

Unknown properties are stripped rather than rejected.

ChatMessage
FieldTypeNotes
id reqstring(uuid)
chatSessionId reqstring(uuid)
role req"agent" | "human" | "system"
text reqstring
seq reqinteger>= 0 · <= 9007199254740991
createdAt reqstring(date-time)
turnIdstring | null
openboolean
stepsobject[]
ChatMessagesResponse
FieldTypeNotes
items reqChatMessage[]
stepsobject[]
ChatSessionsResponse
FieldTypeNotes
items reqChatSessionSummary[]
ChatSessionSummary
FieldTypeNotes
chatSessionId reqstring(uuid)
agentKey reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
status req"active" | "ended"
startedAt reqstring(date-time)
lastMessageAt reqstring(date-time) | null
messageCount reqinteger>= 0 · <= 9007199254740991
kind"team_feed"
displayNameBilingualText
ChatStreamEvent

One of 10 variants:

session.started
FieldTypeNotes
t req"session.started"
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
thought.summary
FieldTypeNotes
t req"thought.summary"
text reqstring
tool.call
FieldTypeNotes
t req"tool.call"
name reqstringmin length 1
summary reqstring
activityBilingualText
tool.result
FieldTypeNotes
t req"tool.result"
name reqstringmin length 1
summary reqstring
activityBilingualText
text.delta
FieldTypeNotes
t req"text.delta"
text reqstring
message_seqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
message_offsetinteger>= 0 · <= 9007199254740991
message.closed
FieldTypeNotes
t req"message.closed"
message_seq reqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
artifact.created
FieldTypeNotes
t req"artifact.created"
kind reqstringmin length 1
path reqstringmin length 1
preview_urlstring(uri)
gate.requested
FieldTypeNotes
t req"gate.requested"
gate_id reqstring(uuid)
session.ended
FieldTypeNotes
t req"session.ended"
result req"done" | "blocked" | "failed"
message.card
FieldTypeNotes
t req"message.card"
card reqobject | object | object
CheckVerificationResponse

One of 2 variants:

true
FieldTypeNotes
verified req"true"
tier req"t0" | "t1" | "t2"
false
FieldTypeNotes
verified req"false"
failure reqobject
Citation
FieldTypeNotes
url reqstring(uri)
domain reqstringmin length 1
class reqenum(5)owned, competitor, ugc, network, other
Comment
FieldTypeNotes
id reqstring(uuid)
ticketId reqstring(uuid)
authorKind req"agent" | "human" | "system"
author reqstringmin length 1
bodyMd reqstring
kind reqenum(6)comment, progress, blocker, handoff, standup, system
mentions reqobject[]
blockerobject
createdAt reqstring(date-time)
CommentCreate
FieldTypeNotes
bodyMd reqstringmin length 1
mentionsobject[]

Unknown properties are stripped rather than rejected.

ConnectResponse

One of 2 variants:

oauth
FieldTypeNotes
kind req"oauth"
oauthUrl reqstring(uri)
credentials
FieldTypeNotes
kind req"credentials"
formSchema reqobject
reason reqBilingualText
downloadPathstring
pairingSecretPathstring
stepsBilingualText[]
ContentDraft mirror

Mirrors packages/app/src/use-cases/content-draft-support.ts (ContentDraftDto). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
vertical reqstring
destination reqstring
countrystring
locales reqmap
status reqenum(5)draft, policy_failed, gated, published, publish_failed
policyVerdictmap
gateIdstring(uuid)
changeIdstring
targetUrlstring
diffStagingDiff
diffArtifactPathstring
previewUrlstring
hasRollbackToken reqboolean
verificationobject
publishedUrlstring
publishedAtstring(date-time)
createdAt reqstring(date-time)
updatedAt reqstring(date-time)
CostPreviewResponse
FieldTypeNotes
priceTableVersion reqstringmin length 1
promptCount reqinteger>= 1 · <= 9007199254740991
samples reqinteger>= 1 · <= 9007199254740991
estimatedCalls reqinteger>= 0 · <= 9007199254740991
engines reqobject[]
range reqobject
unknownEngines reqenum(7)[]
CreateTicketRequest
FieldTypeNotes
title reqstringmin length 1
descriptionMdstring
taskType reqstringmin length 1
assignee reqobject | object
deadlinestring(date)
priority"p0" | "p1" | "p2" | "p3"

Unknown properties are stripped rather than rejected.

CreateWorkspaceRequest
FieldTypeNotes
name reqBilingualTextInput
locale req"ar" | "en"
primaryDomainstring

Unknown properties are stripped rather than rejected.

CredentialsSubmitRequest mirror

Mirrors apps/api/src/integrations/provider-catalog.ts (CredentialsSubmitRequest). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
values reqmap

Unknown properties are stripped rather than rejected.

CsrfTokenResponse mirror

Mirrors apps/api/src/auth/auth.controller.ts (CsrfTokenResponse). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
token reqstring
header reqstring
cookie reqstring
DevLoginRequest mirror

Mirrors apps/api/src/auth/auth.controller.ts (DevLoginRequest). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
email reqstring(email)

Unknown properties are stripped rather than rejected.

EventHistoryPage mirror

Mirrors apps/api/src/events/event-history.controller.ts (EventHistoryPage). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
items reqobject[]
nextCursor reqstring | null
head reqstring
Finding
FieldTypeNotes
id reqstring(uuid)
kind reqstringmin length 1
severity req"info" | "low" | "med" | "high"
evidence reqmap
ticketIdstring(uuid)
FindingsPage mirror

Mirrors apps/api/src/findings/findings.service.ts (FindingsPage). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
items reqFinding[]
nextCursor reqstring | null
Gate
FieldTypeNotes
id reqstring(uuid)
gateType reqenum(9)profile.approved, promptbank.approved, program.approved, content.outline, content.publish, site.write, offsite.publish, placement.purchase, design.approve
status reqenum(5)pending, approved, rejected, expired, auto_approved
summary reqstringmin length 1
ticketId reqstring(uuid)
diffArtifactPathstring
previewUrlstring(uri)
expiresAt reqstring(date-time)
costLineMoney
GateApproveRequest
FieldTypeNotes
notestring

Unknown properties are stripped rather than rejected.

GateDecisionResult
FieldTypeNotes
gateId reqstring(uuid)
status reqenum(5)pending, approved, rejected, expired, auto_approved
decidedAtstring(date-time)
decisionNotestring
GateRejectRequest
FieldTypeNotes
note reqstringmin length 1

Unknown properties are stripped rather than rejected.

HealthResponse mirror

Mirrors apps/api/src/health/health.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
status req"ok"
version reqstring
HireAgentResponse
FieldTypeNotes
status req"hired" | "requires_addon" | "already_hired"
addonenum(7)agent_content, agent_offsite, agent_topic, agent_builder, extra_prompts_100, extra_engine, extra_workspace
IncidentUpdateAck mirror

Mirrors apps/api/src/health/health.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
ok req"true"
IntegrationHealth mirror

Mirrors apps/api/src/integrations/integrations.service.ts (HealthDto). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
status req"pending" | "connected" | "error" | "revoked"
lastCheck reqstring(date-time) | null
error reqstring | null
IntegrationsListResponse
FieldTypeNotes
items reqIntegrationSummary[]
IntegrationSummary
FieldTypeNotes
id reqstring(uuid)
provider reqenum(11)gsc, ga4, wordpress, shopify, webflow, gbp, slack, email, mawdoo3_network, pixel, cloudflare
status req"pending" | "connected" | "error" | "revoked"
lastHealthCheck reqstring(date-time) | null
error reqstring | null
KybDecideRequest mirror

Mirrors apps/api/src/verification/kyb.controller.ts (KybDecideRequest). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
workspaceId reqstring(uuid)
decision req"approve" | "reject"
note reqstringmin length 1

Unknown properties are stripped rather than rejected.

KybQueueResponse mirror

Mirrors apps/api/src/verification/kyb.service.ts (KybQueueItem). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
items reqobject[]
LoginRequest
FieldTypeNotes
email reqstring(email)
password reqstringmin length 1

Unknown properties are stripped rather than rejected.

MarketBrief
FieldTypeNotes
version reqinteger>= 1 · <= 9007199254740991
summary reqstring | null
generatedAt reqstring(date-time)
generatedBy reqstring | null
contentMd reqstring | null
notestring
MarketBriefVersion
FieldTypeNotes
version reqinteger>= 1 · <= 9007199254740991
summary reqstring | null
generatedAt reqstring(date-time)
MembershipSummary
FieldTypeNotes
workspaceId reqstring(uuid)
name reqBilingualText
role req"owner" | "approver" | "viewer"
trustTier req"t0" | "t1" | "t2"
MetricSeries
FieldTypeNotes
metric req"visibility" | "sov" | "citation_share" | "position_score"
engine reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
stratumobject
points reqobject[]
Money
FieldTypeNotes
amountMinor reqinteger>= -9007199254740991 · <= 9007199254740991
currency reqstringpattern ^[A-Z]{3}$
MoneyInput
FieldTypeNotes
amountMinor reqinteger>= -9007199254740991 · <= 9007199254740991
currency reqstringpattern ^[A-Z]{3}$

Unknown properties are stripped rather than rejected.

MonthCostResponse
FieldTypeNotes
period reqobject
totals reqMoney[]
NetworkPropertiesResponse mirror

Mirrors packages/adapters/src/repos/drizzle-network-property.repository.ts (CatalogNetworkProperty). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
items reqobject[]
Notification
FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
userIdstring(uuid)
type reqenum(7)gate_pending, blocker_escalated, standup, weekly_report, plan_proposed, integration_error, win_moment
urgency req"normal" | "urgent"
payload reqmap
channels req"in_app" | "email" | "slack" | "whatsapp"[]min 1 items
readAtstring(date-time) | null
createdAt reqstring(date-time)
NotificationInboxPage mirror

Mirrors apps/api/src/notifications/notification.service.ts (InboxPage). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
items reqNotification[]
nextCursor reqstring | null
NotificationPreferenceMatrix

map

NotificationPreferenceMatrixInput

map

PatchAutonomyRequest
FieldTypeNotes
policies reqobject[]min 1 items

Unknown properties are stripped rather than rejected.

PatchProfileRequest
FieldTypeNotes
namestringmin length 1
nameArstringmin length 1
categoryPathstring
descriptionstring
descriptionArstring
offeringsobject[]
serviceAreasobject[]
languagesstring[]
personasobject[]
socialsobject[]
brandAliasesstring[]
competitorsobject[]
techStackobject
fieldProvenancemap

Unknown properties are stripped rather than rejected.

PatchPromptRequest mirror

Mirrors apps/api/src/measurement/prompt-banks.controller.ts (PatchPromptRequest). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
textstringmin length 1 · max length 500
enabledboolean

Unknown properties are stripped rather than rejected.

PatchSessionMetaRequest mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
agentSessionId reqstringmin length 1 · max length 256

Unknown properties are stripped rather than rejected.

PatchTicketRequest
FieldTypeNotes
boardColumnenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
assigneeobject | object
deadlinestring(date) | null
priority"p0" | "p1" | "p2" | "p3"

Unknown properties are stripped rather than rejected.

PlacementDto
FieldTypeNotes
id reqstring(uuid)
ticketId reqstring(uuid)
propertyKey reqstring | null
kind req"onetime" | "weekly_series"
status reqenum(8)proposed, approved, in_editorial, scheduled, published, live, rejected, cancelled
price reqMoney
publishedUrl reqstring | null
billingEventId reqstring | null
card reqPlacementProposalCard | null
PlacementProposalCard
FieldTypeNotes
property reqobject
fit reqobject
disclosure reqobject
price reqMoney
kind req"onetime" | "weekly_series"
PlacementsListResponse
FieldTypeNotes
items reqPlacementDto[]
PlanResponse
FieldTypeNotes
plan reqobject
entitlements reqobject
usage reqobject[]
ProblemJson
FieldTypeNotes
type reqstring
title reqstring
status reqinteger>= 100 · <= 599
detailstring
instancestring

Unknown properties are stripped rather than rejected.

ProfileApproveResponse mirror

Mirrors apps/api/src/workspaces/profile.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
profileId reqstring(uuid)
status req"draft" | "approved"
activationobject
Prompt
FieldTypeNotes
id reqstring(uuid)
class reqenum(11)discovery, recommendation, comparison, alternative, problem_solution, long_tail_intent, persona, local, use_case, attribute, branded_control
text reqstringmin length 1 · max length 500
language req"ar" | "en"
dialectTagstring
countrystring
funnel req"tofu" | "mofu" | "bofu"
priority req"head" | "tail"
enabled reqboolean
PromptBank
FieldTypeNotes
id reqstring(uuid)
version reqinteger<= 9007199254740991
status req"draft" | "approved" | "frozen" | "superseded"
locale req"ar" | "en"
promptCountinteger>= 0 · <= 9007199254740991
enabledCountinteger>= 0 · <= 9007199254740991
createdAtstring(date-time)
frozenAtstring(date-time)
PromptProvenanceResponse
FieldTypeNotes
promptId reqstring(uuid)
derivation req"seeds" | "template"
seedIds reqstring(uuid)[]
seeds reqSeed[]
PromptStateResponse
FieldTypeNotes
prompts reqobject[]
trackedPrompts reqinteger>= 0 · <= 9007199254740991
PromptTrackingState
FieldTypeNotes
engine reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
regime reqobject
runs reqinteger<= 9007199254740991
mentions reqinteger>= 0 · <= 9007199254740991
mentionRate reqobject
avgRank reqobject | null
sentimentMode req"positive" | "neutral" | "negative" | "mixed" | null
ProposePlacementRequest mirror

Mirrors apps/api/src/placements/placements.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
propertyKey reqstringmin length 1 · max length 200
kind req"onetime" | "weekly_series"
briefMd reqstringmin length 1

Unknown properties are stripped rather than rejected.

ProposePlacementResponse mirror

Mirrors apps/api/src/placements/placements.service.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
placement reqPlacementDto
briefMd reqstring
ProposeSiteChangeRequest mirror

Mirrors apps/api/src/site-changes/site-changes.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
ticketId reqstring(uuid)
changeIdstringmin length 1 · max length 200
changeType req"meta_update" | "schema_inject" | "robots_update" | "content_publish"
targetUrl reqstring(uri)
payloadmap
summarystringmin length 1

Unknown properties are stripped rather than rejected.

ProposeSiteChangeResponse mirror

Mirrors apps/api/src/site-changes/site-changes.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
change reqSiteChange
gateId reqstring(uuid)
replayed reqboolean
ReadinessResponse
FieldTypeNotes
status req"ok" | "degraded"
version reqstring
dependencies reqobject[]
RecordCostsRequest mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
rows reqobject[]min 1 items · max 50 items

Unknown properties are stripped rather than rejected.

RecordCostsResponse mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
recorded reqinteger>= 0 · <= 9007199254740991
RunSummary
FieldTypeNotes
id reqstringmin length 1
bankId reqstring(uuid)
engineKey reqenum(7)chatgpt, gemini, perplexity, claude, copilot, grok, aio
status reqenum(5)queued, running, partial, complete, failed
samplesRequested reqinteger>= 0 · <= 9007199254740991
samplesCompleted reqinteger>= 0 · <= 9007199254740991
createdAt reqstring(date-time)
Sample
FieldTypeNotes
id reqstring(uuid)
promptId reqstring(uuid)
answerText reqstring
citations reqCitation[]
mentioned reqboolean
positioninteger<= 9007199254740991
sentimentinteger>= -100 · <= 100
runnerMode reqenum(5)api_model, api_model_websearch, consumer_surface, direct_api, fal_llm
modelVersion reqstring
ScoresResponse
FieldTypeNotes
weightSet reqstringmin length 1
engines reqobject[]
crs reqobject
Seed
FieldTypeNotes
id reqstring(uuid)
text reqstringmin length 1
language req"ar" | "en"
source req"autocomplete_google" | "dataforseo_paa" | "dataforseo_related" | "gsc"
metricVolumeinteger>= 0 · <= 9007199254740991
evidenceUrlstringmin length 1
evidenceQuotestringmin length 1
confidence reqnumber>= 0 · <= 1
createdAt reqstring(date-time)
SeedsPage
FieldTypeNotes
items reqSeed[]
nextCursor reqstring | null
SendChatMessageRequest
FieldTypeNotes
text reqstringmin length 1 · max length 4000

Unknown properties are stripped rather than rejected.

SendChatMessageResponse
FieldTypeNotes
messageId reqstring(uuid)
seq reqinteger>= 0 · <= 9007199254740991
SessionMetaResponse mirror

Mirrors apps/api/src/internal-agent/internal-agent.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
chatSessionId reqstring(uuid)
agentKey reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
status reqstring
agentSessionId reqstring | null
SignupRequest
FieldTypeNotes
email reqstring(email)
password reqstringmin length 12
namestringmin length 1
locale"ar" | "en"

Unknown properties are stripped rather than rejected.

SiteChange mirror

Mirrors packages/app/src/use-cases/site-change-support.ts (SiteChangeDto). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
changeId reqstring
changeType reqstring
targetUrl reqstring
status reqenum(6)none, applied, verified, verification_failed, committed, rolled_back
gateId reqstring(uuid)
diffArtifactPath reqstring
diff reqStagingDiff
hasRollbackToken reqboolean
verificationobject
createdAt reqstring(date-time)
appliedAtstring(date-time)
rolledBackAtstring(date-time)
StagingDiff mirror

Mirrors packages/app/src/ports/cms-connector.ts (StagingDiff). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
before reqstring
after reqstring
StartAuditResponse
FieldTypeNotes
enqueued req"true"
queue req"vb:audit"
jobId reqstringmin length 1
baseUrl reqstring(uri)
note reqstringmin length 1
StartOnboardingResponse
FieldTypeNotes
chatSessionId reqstring(uuid)
StartRunRequest
FieldTypeNotes
locale"ar" | "en"default "en"
runnerModeenum(5)api_model, api_model_websearch, consumer_surface, direct_api, fal_llm · default "api_model"

Unknown properties are stripped rather than rejected.

StartRunResponse
FieldTypeNotes
enqueued req"true"
queue req"vb:run-bank"
jobId reqstringmin length 1
bankId reqstring(uuid)
bankVersion reqinteger<= 9007199254740991
locale req"ar" | "en"
samples reqinteger>= 1 · <= 10
runnerMode reqenum(5)api_model, api_model_websearch, consumer_surface, direct_api, fal_llm
note reqstringmin length 1
StartVerificationRequest
FieldTypeNotes
domain reqstringmin length 1 · max length 260
method req"dns" | "file"

Unknown properties are stripped rather than rejected.

StartVerificationResponse
FieldTypeNotes
token reqstring
record reqstring
instructions reqBilingualText
StatusIncidentUpdateRequest
FieldTypeNotes
body reqBilingualTextInput

Unknown properties are stripped rather than rejected.

StatusPage
FieldTypeNotes
generatedAt reqstring(date-time)
overall req"operational" | "degraded" | "outage" | "unknown"
overallReason reqenum(6)all_operational, core_outage, core_degraded, vendor_degraded, data_stale, no_data
probeIntervalMs reqinteger<= 9007199254740991
staleAfterMs reqinteger<= 9007199254740991
historyDays reqinteger<= 9007199254740991
components reqobject[]
incidents reqobject[]
source req"live" | "degraded"
SubmitContentDraftRequest mirror

Mirrors apps/api/src/content/content.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
ticketId reqstring(uuid)
vertical reqenum(6)health, finance, insurance, legal, food, general
destination req"client_site" | "network_property" | "gbp" | "social_draft"
countrystringpattern ^[A-Za-z]{2,3}$
draft reqobject
slugstringmin length 1 · max length 200
lexiconobject
corpusTextsstring[]

Unknown properties are stripped rather than rejected.

SubmitContentDraftResponse mirror

Mirrors apps/api/src/content/content.controller.ts. Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
draft reqContentDraft
gateId reqstring(uuid)
TicketDetail
FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
boardColumn reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
title reqstringmin length 1
taskType reqstringmin length 1
assignee reqobject | object
priority req"p0" | "p1" | "p2" | "p3"
deadlinestring(date)
commentCount reqinteger>= 0 · <= 9007199254740991
working reqboolean
blocked reqboolean
descriptionMd reqstring
payload reqmap
gateIdstring(uuid)
temporalWorkflowIdstring
sessionIdstring
TicketEventIngestRequest
FieldTypeNotes
workspaceId reqstring(uuid)
ticketId reqstring(uuid)
events requnion(9)[]min 1 items · max 200 items

Unknown properties are stripped rather than rejected.

TicketEventIngestResponse
FieldTypeNotes
accepted reqinteger>= 0 · <= 9007199254740991
TicketStreamEvent

One of 9 variants:

session.started
FieldTypeNotes
t req"session.started"
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
thought.summary
FieldTypeNotes
t req"thought.summary"
text reqstring
tool.call
FieldTypeNotes
t req"tool.call"
name reqstringmin length 1
summary reqstring
activityBilingualText
tool.result
FieldTypeNotes
t req"tool.result"
name reqstringmin length 1
summary reqstring
activityBilingualText
text.delta
FieldTypeNotes
t req"text.delta"
text reqstring
message_seqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
message_offsetinteger>= 0 · <= 9007199254740991
message.closed
FieldTypeNotes
t req"message.closed"
message_seq reqinteger>= 0 · <= 9007199254740991
turn_idstringmin length 1
artifact.created
FieldTypeNotes
t req"artifact.created"
kind reqstringmin length 1
path reqstringmin length 1
preview_urlstring(uri)
gate.requested
FieldTypeNotes
t req"gate.requested"
gate_id reqstring(uuid)
session.ended
FieldTypeNotes
t req"session.ended"
result req"done" | "blocked" | "failed"
TicketSummary
FieldTypeNotes
id reqstring(uuid)
workspaceId reqstring(uuid)
boardColumn reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled
title reqstringmin length 1
taskType reqstringmin length 1
assignee reqobject | object
priority req"p0" | "p1" | "p2" | "p3"
deadlinestring(date)
commentCount reqinteger>= 0 · <= 9007199254740991
working reqboolean
blocked reqboolean
TicketVerificationState
FieldTypeNotes
ticketId reqstring(uuid)
verification reqobject | null
result reqobject | null
VerificationStatusResponse
FieldTypeNotes
tier req"t0" | "t1" | "t2"
domain reqstring | null
method req"dns" | "file" | "connector" | "gsc" | null
verifiedAt reqstring(date-time) | null
pending reqobject | null
WeeklyDigest
FieldTypeNotes
workspaceId reqstring(uuid)
period reqobject
headline reqBilingualText
metrics reqobject[]
wins reqobject[]
activity reqobject
nextUp reqobject[]
generatedAt reqstring(date-time)
WordPressPairingSecret mirror

Mirrors apps/api/src/integrations/wordpress-pairing-secret.ts (WordPressPairingSecretService.mint). Not yet promoted into @vb/contracts, so this description is maintained rather than derived.

FieldTypeNotes
secret reqstringpattern ^[0-9a-f]{64}$
mintedAt reqstring(date-time)
WorkspaceListResponse
FieldTypeNotes
items reqMembershipSummary[]
WsAckFrame
FieldTypeNotes
t req"ack"
client_op_id reqstringmin length 1
ok reqboolean
errorstringmin length 1
messagestring
WsClientMessage

One of 3 variants:

ticket.move
FieldTypeNotes
t req"ticket.move"
client_op_id reqstringmin length 1
ticket_id reqstring(uuid)
to reqenum(8)backlog, ready, in_progress, blocked, needs_approval, in_review, done, cancelled

Unknown properties are stripped rather than rejected.

comment.create
FieldTypeNotes
t req"comment.create"
client_op_id reqstringmin length 1
ticket_id reqstring(uuid)
comment reqCommentCreate

Unknown properties are stripped rather than rejected.

typing
FieldTypeNotes
t req"typing"
client_op_id reqstringmin length 1
ticket_id reqstring(uuid)

Unknown properties are stripped rather than rejected.

WsErrorFrame
FieldTypeNotes
t req"error"
code reqenum(6)invalid_json, invalid_frame, workspace_query_invalid, workspace_not_found, unauthenticated, workspace_access_denied
message reqstring
WsResumeRequest
FieldTypeNotes
resume_from reqstringmin length 1

Unknown properties are stripped rather than rejected.

WsServerEvent

One of 10 variants:

ticket.updated
FieldTypeNotes
t req"ticket.updated"
event_id reqstringmin length 1
ticket reqTicketSummary
ticket.created
FieldTypeNotes
t req"ticket.created"
event_id reqstringmin length 1
ticket reqTicketSummary
comment.created
FieldTypeNotes
t req"comment.created"
event_id reqstringmin length 1
ticket_id reqstring(uuid)
comment reqComment
agent.status
FieldTypeNotes
t req"agent.status"
event_id reqstringmin length 1
agent_key reqenum(10)onboarding, researcher, prompts, technical, content, offsite, orchestrator, reporting, design, qa
state req"idle" | "working" | "blocked"
ticket_idstring(uuid)
gate.created
FieldTypeNotes
t req"gate.created"
event_id reqstringmin length 1
gate reqGate
gate.resolved
FieldTypeNotes
t req"gate.resolved"
event_id reqstringmin length 1
gate_id reqstring(uuid)
status reqenum(5)pending, approved, rejected, expired, auto_approved
blocker.raised
FieldTypeNotes
t req"blocker.raised"
event_id reqstringmin length 1
ticket_id reqstring(uuid)
blocker reqobject
standup.posted
FieldTypeNotes
t req"standup.posted"
event_id reqstringmin length 1
comment_id reqstring(uuid)
connector.suggested
FieldTypeNotes
t req"connector.suggested"
event_id reqstringmin length 1
workspaceId reqstring(uuid)
provider reqenum(7)wordpress, shopify, webflow, wix, squarespace, nextjs, unknown
confidence req"confirmed" | "likely"
evidence reqobject[]max 20 items
message reqBilingualText
occurredAt reqstring(date-time)
presence
FieldTypeNotes
t req"presence"
event_id reqstringmin length 1
users reqobject[]