API reference
Public endpoint schemas, authentication, scopes, and request examples.
On this page
Endpoint descriptions and schema names are shared between languages.
Health & Workers
Service health checks and connected worker status monitoring.
GET/healthHealth checkNo auth
Returns a plain-text OK response. Use this to verify the backend is running and reachable.
# GET /healthResponse body
| Field | Type / status | Required | Description |
|---|---|---|---|
body | string | Required | Plain-text "ok" response. |
Content types: application/json → application/json
cURL example
curl http://localhost:8080/healthResponse example
"ok"GET/api/workers/statusGet connected workersUser token
Returns project-scoped worker availability. Busy workers are only included when they are running an execution for the requested project; idle workers are anonymized.
# GET /api/workers/statusQuery parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
project_id | uuid | Required | Project whose worker availability should be returned. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
connected | number | Required | Number of workers visible for the requested project. |
workers | WorkerInfo[] | Required | Array of project-visible worker details. |
workers[].worker_id | string | Required | Unique worker identifier for project-busy workers, or an anonymized availability identifier for idle workers. |
workers[].status | string | Required | Worker status: "idle" or "busy". |
workers[].connected_at | string | Optional | ISO 8601 timestamp of connection time for project-busy workers. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
"http://localhost:8080/api/workers/status?project_id=PROJECT_ID"Response example
{
"connected": 2,
"workers": [
{
"worker_id": "available-1",
"worker_name": "Available worker",
"status": "idle"
},
{
"worker_id": "worker-2",
"worker_name": "Project worker",
"status": "busy",
"connected_at": "2026-04-04T09:45:00Z"
}
]
}Workspaces
Manage workspaces. Users can start with no workspaces; each workspace owns its projects and workers.
GET/api/workspacesList workspacesUser token
Returns workspace_members rows for the authenticated user, with each workspace embedded under the workspaces field.
# GET /api/workspacesResponse body
| Field | Type / status | Required | Description |
|---|---|---|---|
workspace_id | string | Required | Workspace UUID from workspace_members. |
role | string | Required | User role in the workspace. |
workspaces | object | Required | Embedded workspace: id, name, slug, plan, max_concurrent_executions, created_at. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/workspacesResponse example
[
{
"workspace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"role": "owner",
"workspaces": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corp",
"slug": "acme-corp",
"plan": "free",
"max_concurrent_executions": 10,
"created_at": "2026-01-15T08:30:00Z"
}
}
]POST/api/workspacesCreate workspaceUser token
Creates a new workspace, inserts the caller as owner, and optionally sets an workspace execution concurrency limit.
# POST /api/workspacesRequest body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Display name for the new workspace. |
max_concurrent_executions | number | Optional | Optional workspace-wide running execution limit. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | UUID of the newly created workspace. |
name | string | Required | Workspace name. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"New Workspace","max_concurrent_executions":5}' \
http://localhost:8080/api/workspacesResponse example
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "New Workspace",
"slug": "new-workspace-1a2b3c4d",
"plan": "free",
"max_concurrent_executions": 5,
"created_at": "2026-04-04T12:00:00Z"
}PUT/api/workspaces/{workspaceId}Update workspaceUser token
Updates workspace metadata and workspace-wide execution concurrency for an owner/admin JWT session.
# PUT /api/workspaces/{workspaceId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
workspaceId | string | Required | UUID of the workspace. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | New display name. |
max_concurrent_executions | number | Optional | Workspace-wide running execution limit. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Workspace UUID. |
name | string | Required | Workspace name. |
max_concurrent_executions | number | Required | Workspace-wide running execution limit. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_concurrent_executions":5}' \
http://localhost:8080/api/workspaces/b2c3d4e5-f6a7-8901-bcde-f12345678901Response example
[
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "New Workspace",
"max_concurrent_executions": 5
}
]GET/api/workspaces/{workspaceId}/statsGet workspace statsUser token
Returns aggregate execution and job stats across all projects in a workspace.
# GET /api/workspaces/{workspaceId}/statsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
workspaceId | string | Required | UUID of the workspace. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
project_count | number | Required | Projects in the workspace. |
total_runs | number | Required | Total executions. |
passed | number | Required | Passed executions. |
failed | number | Required | Failed executions. |
errors | number | Required | Errored executions. |
running | number | Required | Running or pending executions. |
avg_duration_ms | number | Required | Average completed run duration. |
last_run_at | string | null | Required | Most recent execution creation time. |
pass_rate | number | Required | Pass percentage across terminal runs. |
job_count | number | Required | Saved jobs in the workspace. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/workspaces/b2c3d4e5-f6a7-8901-bcde-f12345678901/statsResponse example
{
"project_count": 3,
"total_runs": 128,
"passed": 118,
"failed": 7,
"errors": 3,
"running": 1,
"avg_duration_ms": 2410,
"last_run_at": "2026-05-02T15:30:00Z",
"pass_rate": 92.2,
"job_count": 14
}GET/api/workspaces/{workspaceId}/project-healthGet workspace project healthUser token
Returns the latest 12 execution outcomes and current active-run count for every project the authenticated workspace member can access.
# GET /api/workspaces/{workspaceId}/project-healthPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
workspaceId | string | Required | UUID of the workspace. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
project_id | string | Required | Accessible project UUID. |
recent_runs | array | Required | Up to 12 recent status and creation-time records, oldest first. |
recent_pass_rate | number | Required | Pass percentage across recent terminal runs. |
running | number | Required | All currently pending or running executions. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/workspaces/b2c3d4e5-f6a7-8901-bcde-f12345678901/project-healthResponse example
[
{
"project_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"recent_runs": [
{
"status": "failed",
"created_at": "2026-08-19T10:00:00Z"
},
{
"status": "passed",
"created_at": "2026-08-19T11:00:00Z"
}
],
"recent_pass_rate": 50,
"running": 0
}
]Projects
Manage projects within workspaces. Projects group jobs, test runs, triggers, and queues.
GET/api/workspaces/{workspaceId}/projectsList workspace projectsUser token
Returns all projects in the workspace for a browser-authenticated workspace member.
# GET /api/workspaces/{workspaceId}/projectsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
workspaceId | string | Required | UUID of the workspace. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Project UUID. |
name | string | Required | Project name. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/projectsResponse example
[
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"name": "Website QA",
"created_at": "2026-02-01T09:00:00Z"
}
]POST/api/workspaces/{workspaceId}/projectsCreate projectUser token
Creates a new project within the specified workspace for an owner/admin JWT session.
# POST /api/workspaces/{workspaceId}/projectsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
workspaceId | string | Required | UUID of the workspace. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Project name. |
max_concurrent_executions | number | Optional | Optional project-level running execution limit. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | UUID of the newly created project. |
name | string | Required | Project name. |
workspace_id | string | Required | UUID of the owning workspace. |
max_concurrent_executions | number | null | Optional | Project-level running execution limit, if set. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Checkout Flow RPA","max_concurrent_executions":3}' \
http://localhost:8080/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/projectsResponse example
[
{
"id": "d4e5f6a7-b8c9-0123-defa-234567890123",
"workspace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Checkout Flow RPA",
"max_concurrent_executions": 3,
"created_at": "2026-04-04T12:30:00Z"
}
]GET/api/projectsList all user projectsUser token or API key
Returns all projects the authenticated user has access to, across all workspaces.
# GET /api/projectsResponse body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Project UUID. |
name | string | Required | Project name. |
project_type | string | Required | Project type. |
workspace_id | string | Required | UUID of the owning workspace. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projectsResponse example
[
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"name": "Website QA",
"project_type": "qa",
"workspace_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"created_at": "2026-02-01T09:00:00Z"
}
]PUT/api/projects/{projectId}Update projectUser token
Updates project metadata and clears or sets project-level execution concurrency for an owner/admin JWT session.
# PUT /api/projects/{projectId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | New project name. |
max_concurrent_executions | number | null | Optional | Project running execution limit, or null to inherit from the workspace. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Project UUID. |
name | string | Required | Project name. |
max_concurrent_executions | number | null | Optional | Project-level running execution limit. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_concurrent_executions":null}' \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012Response example
[
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"name": "Website QA",
"max_concurrent_executions": null
}
]GitHub
Connect a project to a GitHub App installation, browse repository state, and export BernOps-managed tests as draft pull requests.
GET/api/github/setupComplete GitHub setupNo auth
GitHub redirects to this endpoint during OAuth authorization and again after installation. It validates the signed setup state, verifies the installing user can access the returned installation, records the installation, and redirects back to the frontend project GitHub page.
# GET /api/github/setupQuery parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
state | string | Required | Signed setup state returned by GitHub from the authorization or installation URL. |
code | string | Optional | GitHub OAuth code returned before installation so BernOps can obtain a user access token for setup verification. |
installation_id | number | Optional | GitHub App installation ID assigned by GitHub after OAuth authorization completes. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
Location | string | Required | Frontend redirect URL containing the setup result query parameters. |
Content types: application/json → application/json
cURL example
# Browser redirect endpoint. Start setup through:
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:8080/api/projects/PROJECT_ID/github/install-urlResponse example
HTTP/1.1 302 Found
Location: http://app.bernops-dev/projects/github?projectId=PROJECT_ID&github=installedPOST/api/github/webhooksReceive GitHub webhookNo auth
Receives GitHub App webhook deliveries, validates the X-Hub-Signature-256 HMAC signature, deduplicates deliveries, and syncs installation, repository, and pull request state. Processing failures are recorded and returned as non-2xx responses so GitHub can retry.
# POST /api/github/webhooksRequest body
| Field | Type / status | Required | Description |
|---|---|---|---|
payload | GitHub webhook JSON | Required | Raw GitHub webhook payload. The exact shape depends on the X-GitHub-Event header. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
accepted | boolean | Required | Whether the delivery was accepted for processing. |
delivery_id | string | Required | GitHub delivery ID from X-GitHub-Delivery. |
status | string | Optional | Processing status recorded for the delivery. |
Content types: application/json → application/json
cURL example
curl -X POST -H "X-GitHub-Event: installation" \
-H "X-GitHub-Delivery: delivery-001" \
-H "X-Hub-Signature-256: sha256=CALCULATED_SIGNATURE" \
-H "Content-Type: application/json" \
-d '{"action":"created"}' \
http://localhost:8080/api/github/webhooksResponse example
{
"accepted": true,
"delivery_id": "delivery-001",
"status": "processed"
}POST/api/projects/{projectId}/github/install-urlCreate install URLUser token
Creates a short-lived signed setup state and returns the GitHub App user authorization URL for a workspace owner/admin. The OAuth step is required before installation so BernOps can verify the returned installation belongs to the installing GitHub user.
# POST /api/projects/{projectId}/github/install-urlPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project to connect. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
url | string | Required | GitHub App user authorization URL containing the signed state. |
expires_at | string | Required | ISO 8601 expiry timestamp for the setup state. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/PROJECT_ID/github/install-urlResponse example
{
"url": "https://github.com/login/oauth/authorize?client_id=Iv1.example&redirect_uri=https%3A%2F%2Fapi.bernops.example%2Fapi%2Fgithub%2Fsetup&state=SIGNED_STATE",
"expires_at": "2026-05-03T12:10:00Z"
}GET/api/projects/{projectId}/github/statusGet GitHub statusUser token
Returns the project GitHub connection state, available workspace installations, selected repository, pull request sync state, whether the current user can manage the project connection, and whether they can install the workspace GitHub App.
# GET /api/projects/{projectId}/github/statusPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
installed | boolean | Required | Whether the workspace has at least one GitHub App installation. |
connected | boolean | Required | Whether the project has a selected repository. |
installations | GitHubInstallation[] | Required | Workspace installations visible to the project. |
repository | GitHubRepository | null | Optional | Selected project repository, if connected. |
pull_requests | GitHubPullRequest[] | Required | Tracked BernOps export pull requests for the selected repository. |
can_manage | boolean | Required | Whether the current user can manage the GitHub connection. |
can_install | boolean | Required | Whether the current user can create workspace-level GitHub App install URLs. |
default_folder | string | Required | Default folder used when selecting a repository. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/PROJECT_ID/github/statusResponse example
{
"installed": true,
"connected": true,
"installations": [
{
"id": "installation-001",
"account_login": "acme"
}
],
"repository": {
"repo_full_name": "acme/web",
"html_url": "https://github.com/acme/web",
"default_branch": "main",
"test_folder_path": "bernops/e2e"
},
"pull_requests": [],
"can_manage": true,
"can_install": true,
"default_folder": "bernops/e2e"
}DELETE/api/projects/{projectId}/githubDisconnect repositoryUser token
Disconnects the selected GitHub repository from a project. The workspace installation remains available for reconnecting this or another repository.
# DELETE /api/projects/{projectId}/githubPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
installed | boolean | Required | Whether a workspace installation remains available. |
connected | boolean | Required | Whether the project still has a selected repository. |
installations | GitHubInstallation[] | Required | Workspace installations still available after disconnecting. |
repository | GitHubRepository | null | Optional | Selected repository after disconnecting, normally null. |
pull_requests | GitHubPullRequest[] | Required | Tracked pull requests after disconnecting, normally empty. |
can_manage | boolean | Required | Whether the current user can manage the GitHub connection. |
can_install | boolean | Required | Whether the current user can create workspace-level GitHub App install URLs. |
default_folder | string | Required | Default export folder used by repository selection. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/PROJECT_ID/githubResponse example
{
"installed": true,
"connected": false,
"repository": null,
"pull_requests": []
}GET/api/projects/{projectId}/github/reposList GitHub repositoriesUser token
Lists repositories available through each workspace GitHub App installation so a project admin can select one for BernOps sync.
# GET /api/projects/{projectId}/github/reposPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
installations | Array<{ installation: GitHubInstallation; repositories: GitHubRepository[] }> | Required | Repositories grouped by installation. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/PROJECT_ID/github/reposResponse example
{
"installations": [
{
"installation": {
"id": "installation-001",
"account_login": "acme"
},
"repositories": [
{
"id": 123,
"full_name": "acme/web",
"default_branch": "main"
}
]
}
]
}POST/api/projects/{projectId}/github/repos/selectSelect repositoryUser token
Connects a project to one repository exposed by a workspace GitHub App installation and stores the folder where BernOps-managed tests are exported.
# POST /api/projects/{projectId}/github/repos/selectPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
installation_id | string | Required | BernOps installation row UUID returned by repository listing. |
github_repo_id | number | Required | GitHub repository ID to connect. |
test_folder_path | string | Optional | Repository folder for BernOps test exports. Defaults to bernops/e2e. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
connected | boolean | Required | Whether the project now has a selected repository. |
installed | boolean | Required | Whether the workspace has a GitHub App installation. |
installations | GitHubInstallation[] | Required | Workspace installations visible to the project. |
repository | GitHubRepository | Required | Stored repository connection. |
pull_requests | GitHubPullRequest[] | Required | Tracked BernOps pull requests for the selected repository. |
can_manage | boolean | Required | Whether the current user can manage the GitHub connection. |
can_install | boolean | Required | Whether the current user can create workspace-level GitHub App install URLs. |
default_folder | string | Required | Default export folder used by repository selection. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"installation_id":"installation-001","github_repo_id":123,"test_folder_path":"bernops/e2e"}' \
http://localhost:8080/api/projects/PROJECT_ID/github/repos/selectResponse example
{
"installed": true,
"connected": true,
"repository": {
"repo_full_name": "acme/web",
"html_url": "https://github.com/acme/web",
"default_branch": "main",
"test_folder_path": "bernops/e2e"
}
}GET/api/projects/{projectId}/github/treeGet repository treeUser token
Reads the selected repository tree through the project GitHub App installation. Use recursive=true to request a recursive tree from GitHub.
# GET /api/projects/{projectId}/github/treePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
ref | string | Optional | Branch, tag, or SHA to read. Defaults to the selected repository default branch. |
recursive | boolean | Optional | Whether GitHub should return the tree recursively. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
sha | string | Required | Tree SHA returned by GitHub. |
entries | GitHubTreeEntry[] | Required | Repository tree entries. |
truncated | boolean | Optional | Whether GitHub truncated the tree response. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
"http://localhost:8080/api/projects/PROJECT_ID/github/tree?recursive=true"Response example
{
"sha": "tree-sha",
"entries": [
{
"path": "bernops/e2e/login.spec.ts",
"type": "blob",
"sha": "file-sha"
}
],
"truncated": false
}POST/api/projects/{projectId}/github/export-testsExport tests to GitHubUser token
Exports BernOps-managed tests to the selected repository, commits them to a BernOps branch, and opens or updates a draft pull request.
# POST /api/projects/{projectId}/github/export-testsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
pull_request | GitHubPullRequest | Required | Created or updated GitHub pull request. |
commit | GitHubCommit | Required | Commit created on the BernOps export branch. |
exported | Array<{ job_id: string; path: string }> | Required | Tests written to GitHub. |
skipped | Array<{ job_id: string; path?: string; reason: string }> | Required | Tests skipped because they were empty, duplicated, or conflicted with remote content. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/PROJECT_ID/github/export-testsResponse example
{
"pull_request": {
"number": 42,
"url": "https://github.com/acme/web/pull/42",
"draft": true
},
"commit": {
"branch_name": "bernops/update-tests-20260503121000-ab12cd34",
"commit_sha": "commit-sha",
"tree_sha": "tree-sha"
},
"exported": [
{
"job_id": "job-001",
"path": "bernops/e2e/login.spec.ts"
}
],
"skipped": []
}GET/api/projects/{projectId}/github/pull-requestsList export pull requestsUser token
Returns BernOps-tracked GitHub export pull requests for the selected project repository.
# GET /api/projects/{projectId}/github/pull-requestsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
pull_requests | GitHubPullRequest[] | Required | Tracked pull request records for the selected repository. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/PROJECT_ID/github/pull-requestsResponse example
{
"pull_requests": [
{
"branch_name": "bernops/update-tests-20260503121000-ab12cd34",
"status": "open",
"draft": true,
"pr_number": 42,
"pr_url": "https://github.com/acme/web/pull/42"
}
]
}Project Variables
Manage project runtime variables. Secret values are encrypted, write-only, and only injected into authorized executions.
GET/api/projects/{projectId}/variablesList project variablesUser token
Returns variable metadata and plain non-secret values for a project. Secret values are never returned.
# GET /api/projects/{projectId}/variablesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Variable UUID. |
name | string | Required | Environment variable name. |
is_secret | boolean | Required | Whether the value is encrypted and write-only. |
value | string | null | Optional | Plain value for non-secret variables only. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/project-001/variablesResponse example
[
{
"id": "var-001",
"name": "BASE_URL",
"is_secret": false,
"value": "https://app.example.com"
},
{
"id": "var-002",
"name": "API_TOKEN",
"is_secret": true,
"value": null
}
]POST/api/projects/{projectId}/variablesCreate project variableUser token
Creates a plain or secret runtime variable. Project admin access is required.
# POST /api/projects/{projectId}/variablesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Uppercase env var name. |
description | string | Optional | Operator-facing description. |
is_secret | boolean | Required | Encrypt and hide the value after save. |
value | string | Required | Variable value to store. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Variable UUID. |
name | string | Required | Environment variable name. |
is_secret | boolean | Required | Whether the value is secret. |
secret_version | number | Required | Secret version counter. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"API_TOKEN","description":"CI token","is_secret":true,"value":"secret-value"}' \
http://localhost:8080/api/projects/project-001/variablesResponse example
{
"id": "var-002",
"name": "API_TOKEN",
"is_secret": true,
"value": null,
"secret_version": 1
}PUT/api/project-variables/{variableId}Update project variableUser token
Updates variable metadata or value. Updating a secret value increments its secret version.
# PUT /api/project-variables/{variableId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
variableId | string | Required | UUID of the variable. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | New uppercase env var name. |
description | string | Optional | New description. |
value | string | Optional | New value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Variable UUID. |
name | string | Required | Environment variable name. |
secret_version | number | Required | Secret version counter. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description":"Rotated token"}' \
http://localhost:8080/api/project-variables/var-002Response example
{
"id": "var-002",
"name": "API_TOKEN",
"is_secret": true,
"value": null,
"secret_version": 2
}DELETE/api/project-variables/{variableId}Delete project variableUser token
Deletes a project variable after project admin access checks.
# DELETE /api/project-variables/{variableId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
variableId | string | Required | UUID of the variable. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
deleted | boolean | Required | Deletion confirmation. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/project-variables/var-002Response example
{
"deleted": true
}POST/api/project-variables/{variableId}/rotateRotate secret variableUser token
Replaces the value for a secret variable and increments its version.
# POST /api/project-variables/{variableId}/rotatePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
variableId | string | Required | UUID of the secret variable. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
value | string | Required | New secret value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Variable UUID. |
secret_version | number | Required | Incremented secret version. |
last_rotated_at | string | Required | Rotation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"new-secret-value"}' \
http://localhost:8080/api/project-variables/var-002/rotateResponse example
{
"id": "var-002",
"name": "API_TOKEN",
"is_secret": true,
"value": null,
"secret_version": 3
}Jobs
Manage Playwright test jobs within a project. Jobs contain the test code that can be executed on demand or via triggers.
GET/api/projects/{projectId}/jobsList jobsUser token or API key
Returns all jobs belonging to the specified project.
# GET /api/projects/{projectId}/jobsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Job UUID. |
name | string | Required | Job display name. |
description | string | Optional | Optional description. |
file_name | string | Optional | Original file name. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/jobsResponse example
[
{
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"name": "Login Flow",
"description": "Tests the sign-in flow end to end.",
"file_name": "login.spec.ts",
"created_at": "2026-03-10T14:00:00Z"
}
]POST/api/projects/{projectId}/jobsCreate jobUser token or API key
Creates a new job within the specified project. Project API keys need jobs:write. The code field contains the Playwright test source.
# POST /api/projects/{projectId}/jobsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Job display name. |
description | string | Optional | Optional description of what the job tests. |
code | string | Required | Playwright test source code. |
file_name | string | Optional | Original file name (e.g. login.spec.ts). |
max_concurrent_executions | number | null | Optional | Optional per-job concurrency limit. Null inherits project and workspace limits. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | UUID of the newly created job. |
name | string | Required | Job name. |
code | string | Required | Job source code. |
created_at | string | Required | ISO 8601 creation timestamp. |
max_concurrent_executions | number | null | Optional | Per-script concurrency limit, when configured. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Login Flow","code":"import { test } from ...","file_name":"login.spec.ts","max_concurrent_executions":1}' \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/jobsResponse example
{
"id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
"name": "Login Flow",
"code": "import { test } from '@playwright/test'; ...",
"max_concurrent_executions": 1,
"created_at": "2026-04-04T13:00:00Z"
}GET/api/jobs/{jobId}Get jobUser token or API key
Returns the full details of a single job, including its code.
# GET /api/jobs/{jobId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
jobId | string | Required | UUID of the job. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Job UUID. |
name | string | Required | Job name. |
description | string | Optional | Optional description. |
code | string | Required | Playwright test source code. |
file_name | string | Optional | Original file name. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/jobs/e5f6a7b8-c9d0-1234-efab-345678901234Response example
{
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"name": "Login Flow",
"description": "Tests the sign-in flow end to end.",
"code": "import { test } from '@playwright/test'; ...",
"file_name": "login.spec.ts",
"created_at": "2026-03-10T14:00:00Z"
}PUT/api/jobs/{jobId}Update jobUser token or API key
Updates an existing job. Project API keys need jobs:write. All fields in the request body are optional; only provided fields are updated.
# PUT /api/jobs/{jobId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
jobId | string | Required | UUID of the job. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | New job name. |
description | string | Optional | New description. |
code | string | Optional | Updated Playwright test source code. |
file_name | string | Optional | Updated file name. |
max_concurrent_executions | number | null | Optional | Set or clear the per-job concurrency limit. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Job UUID. |
name | string | Required | Updated job name. |
code | string | Required | Updated source code. |
max_concurrent_executions | number | null | Optional | Per-script concurrency limit, when configured. |
updated_at | string | Required | ISO 8601 update timestamp. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Login Flow v2","code":"import { test } from ...","max_concurrent_executions":1}' \
http://localhost:8080/api/jobs/e5f6a7b8-c9d0-1234-efab-345678901234Response example
{
"id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"name": "Login Flow v2",
"code": "import { test } from '@playwright/test'; ...",
"max_concurrent_executions": 1,
"updated_at": "2026-04-04T14:00:00Z"
}DELETE/api/jobs/{jobId}Delete jobUser token or API key
Permanently deletes a job and its association with triggers. Project API keys need jobs:write. Existing test run history is preserved.
# DELETE /api/jobs/{jobId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
jobId | string | Required | UUID of the job to delete. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
message | string | Required | Confirmation message. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/jobs/e5f6a7b8-c9d0-1234-efab-345678901234Response example
{
"message": "Job deleted successfully."
}GET/api/jobs/{jobId}/runsList job runsUser token or API key
Returns all test runs associated with a specific job, ordered by most recent first.
# GET /api/jobs/{jobId}/runsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
jobId | string | Required | UUID of the job. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Test run execution ID. |
status | string | Required | Run status: "pending", "running", "passed", "failed", "cancelled". |
started_at | string | Optional | ISO 8601 start timestamp. |
completed_at | string | Optional | ISO 8601 completion timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/jobs/e5f6a7b8-c9d0-1234-efab-345678901234/runsResponse example
[
{
"id": "run-001",
"status": "passed",
"started_at": "2026-04-04T10:00:00Z",
"completed_at": "2026-04-04T10:02:30Z"
}
]POST/api/jobs/{jobId}/executeExecute jobUser token or API key
Starts a test run from a saved job and dispatches it to an available worker. Project API keys need `jobs:execute`; keys with `variables:use` include secret project variables by default unless `include_secret_variables` is false. JWT callers only include secrets when `include_secret_variables` is explicitly true.
# POST /api/jobs/{jobId}/executePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
jobId | string | Required | UUID of the saved job to execute. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
base_url | string | Optional | Base URL override for this run. |
include_secret_variables | boolean | Optional | Whether to include secret project variables. Defaults to true for API keys with `variables:use`, false otherwise. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
execution_id | string | Required | Unique execution ID for this test run. |
status | string | Required | Initial status, typically "pending" or "running". |
worker_id | string | Optional | ID of the assigned worker, if dispatched immediately. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"base_url":"https://staging.example.com"}' \
http://localhost:8080/api/jobs/e5f6a7b8-c9d0-1234-efab-345678901234/executeResponse example
{
"execution_id": "exec-a1b2c3d4",
"status": "running",
"worker_id": "worker-1"
}GET/api/jobs/{jobId}/statsGet job statsUser token or API key
Returns aggregated statistics for a job: total runs, pass rate, average duration.
# GET /api/jobs/{jobId}/statsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
jobId | string | Required | UUID of the job. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
total_runs | number | Required | Total number of runs for this job. |
pass_rate | number | Required | Percentage of passed runs (0-100). |
avg_duration_ms | number | Required | Average run duration in milliseconds. |
last_run_at | string | Optional | ISO 8601 timestamp of the most recent run. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/jobs/e5f6a7b8-c9d0-1234-efab-345678901234/statsResponse example
{
"total_runs": 42,
"pass_rate": 95.2,
"avg_duration_ms": 15200,
"last_run_at": "2026-04-04T10:02:30Z"
}Test Runs
Execute and monitor Playwright test runs. Runs are dispatched to connected workers and stream logs and steps in real time.
POST/api/projects/{projectId}/test-runsStart test runUser token or API key
Creates a new test run and dispatches it to an available worker. Provide `test_files` for an ad-hoc run or a `job_id` to execute a saved job.
# POST /api/projects/{projectId}/test-runsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
test_files | TestFile[] | Optional | Inline Playwright files to execute. Each entry should include `file_name` and `code`. |
base_url | string | Optional | Base URL override for this run. |
job_id | string | Optional | UUID of a saved job to execute. When present, `jobs:execute` is sufficient. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
execution_id | string | Required | Unique execution ID for this test run. |
status | string | Required | Initial status (typically "pending" or "running"). |
worker_id | string | Optional | ID of the assigned worker, if dispatched immediately. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"test_files":[{"file_name":"checkout.spec.ts","code":"import { test } from "@playwright/test";"}],"base_url":"https://staging.example.com"}' \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/test-runsResponse example
{
"execution_id": "exec-a1b2c3d4",
"status": "running",
"worker_id": "worker-1"
}GET/api/projects/{projectId}/test-runsList test runsUser token or API key
Returns test runs for the specified project, ordered by most recent first. Job-scoped API keys only receive runs for their bound external job.
# GET /api/projects/{projectId}/test-runsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Test run execution ID. |
status | string | Required | Run status. |
job_id | string | Optional | Associated job UUID, if any. |
started_at | string | Optional | ISO 8601 start timestamp. |
completed_at | string | Optional | ISO 8601 completion timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/test-runsResponse example
[
{
"id": "exec-a1b2c3d4",
"status": "passed",
"job_id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"started_at": "2026-04-04T13:00:00Z",
"completed_at": "2026-04-04T13:02:15Z"
}
]GET/api/projects/{projectId}/statsGet project statsUser token or API key
Returns exact aggregate execution statistics for the project. Job-scoped API keys receive only their bound external job's execution totals.
# GET /api/projects/{projectId}/statsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
total_runs | number | Required | Total test runs in this project. |
pass_rate | number | Required | Percentage of passed runs (0-100). |
passed | number | Required | Passed executions. |
failed | number | Required | Failed executions. |
errors | number | Required | Errored executions. |
running | number | Required | Pending or running executions. |
avg_duration_ms | number | Required | Average duration across executions that reported one. |
last_run_at | string | null | Required | Most recent execution creation time. |
job_count | number | Required | Saved jobs in the project for unscoped callers. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/statsResponse example
{
"total_runs": 156,
"passed": 144,
"failed": 7,
"errors": 5,
"running": 0,
"avg_duration_ms": 2410,
"last_run_at": "2026-08-19T12:00:00Z",
"pass_rate": 92.3,
"job_count": 12
}GET/api/projects/{projectId}/run-activityGet project run activityUser token or API key
Returns exact daily outcome buckets for the requested 1-30 day window. Dates use the requested IANA time zone, and job-scoped API keys receive only their bound external job's executions.
# GET /api/projects/{projectId}/run-activityPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
days | integer | Optional | Number of daily buckets to return, from 1 through 30. (Default: 7) |
timezone | string | Optional | IANA time zone used to determine each execution's calendar date. (Default: UTC) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
[].date | string | Required | Calendar date in YYYY-MM-DD format. |
[].passed | number | Required | Executions that passed on this date. |
[].unsuccessful | number | Required | Executions that failed or errored on this date. |
[].active | number | Required | Executions that are pending or running on this date. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
"http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/run-activity?days=7&timezone=America%2FToronto"Response example
[
{
"date": "2026-08-18",
"passed": 18,
"unsuccessful": 2,
"active": 0
},
{
"date": "2026-08-19",
"passed": 11,
"unsuccessful": 1,
"active": 2
}
]GET/api/projects/{projectId}/dashboard-runsGet project dashboard run summariesUser token or API key
Returns the latest 60 lightweight run summaries and the latest execution for each saved job without transferring execution results or runtime variables.
# GET /api/projects/{projectId}/dashboard-runsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
recent_runs | RunSummary[] | Required | At most 60 runs ordered newest first. |
latest_job_runs | RunSummary[] | Required | Latest execution for each saved job that has run. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/dashboard-runsResponse example
{
"recent_runs": [
{
"id": "exec-a1b2c3d4",
"job_id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"status": "passed",
"duration_ms": 2410,
"created_at": "2026-08-19T12:00:00Z"
}
],
"latest_job_runs": [
{
"id": "exec-a1b2c3d4",
"job_id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"status": "passed",
"duration_ms": 2410,
"created_at": "2026-08-19T12:00:00Z"
}
]
}GET/api/test-runs/{executionId}Get test run detailsUser token or API key
Returns detailed information about a specific test run, including its current status and timing.
# GET /api/test-runs/{executionId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution ID of the test run. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
[] | TestExecution[] | Required | Array containing the matching execution record. |
[].id | string | Required | Test run execution ID. |
[].status | string | Required | Current run status. |
[].project_id | string | Required | UUID of the parent project. |
[].progress | ProgressSnapshot | null | Required | Latest canonical progress snapshot, or null when no progress was reported. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/test-runs/exec-a1b2c3d4Response example
[
{
"id": "exec-a1b2c3d4",
"status": "passed",
"project_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"worker_id": "worker-1",
"started_at": "2026-04-04T13:00:00Z",
"finished_at": "2026-04-04T13:02:15Z"
}
]GET/api/test-runs/{executionId}/logsGet test run logsUser token or API key
Returns the console output and log entries from a test run execution.
# GET /api/test-runs/{executionId}/logsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution ID of the test run. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | number | Optional | Maximum number of log rows to return. (Default: 50) |
offset | number | Optional | Pagination offset. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | LogEntry[] | Required | Array of log entries. |
total | number | Required | Total number of log rows for the execution. |
data[].timestamp | string | Required | ISO 8601 timestamp of the log entry. |
data[].level | string | Required | Log level: "info", "warn", "error", "debug". |
data[].message | string | Required | Log message content. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/test-runs/exec-a1b2c3d4/logsResponse example
{
"data": [
{
"timestamp": "2026-04-04T13:00:01Z",
"level": "info",
"message": "Running test: login.spec.ts"
},
{
"timestamp": "2026-04-04T13:02:15Z",
"level": "info",
"message": "All tests passed."
}
],
"total": 2,
"limit": 50,
"offset": 0
}GET/api/test-runs/{executionId}/stepsGet test run stepsUser token or API key
Returns the individual test steps and their results from a test run execution.
# GET /api/test-runs/{executionId}/stepsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution ID of the test run. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | number | Optional | Maximum number of step rows to return. (Default: 50) |
offset | number | Optional | Pagination offset. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | TestStep[] | Required | Array of test steps. |
total | number | Required | Total number of stored step rows for the execution. |
data[].event | string | Required | Step event name such as test:start, step:start, or step:end. |
data[].title | string | Optional | Display title for the step event. |
data[].duration_ms | number | Optional | Step duration in milliseconds. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/test-runs/exec-a1b2c3d4/stepsResponse example
{
"data": [
{
"event": "step:start",
"title": "navigate to /login",
"status": "running",
"duration_ms": 320
},
{
"event": "step:end",
"title": "fill #email",
"status": "passed",
"duration_ms": 45
}
],
"total": 2,
"limit": 50,
"offset": 0
}POST/api/test-runs/{executionId}/cancelCancel test runUser token or API key
Marks a running or pending test run as cancelled and broadcasts the updated status to connected clients.
# POST /api/test-runs/{executionId}/cancelPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution ID of the test run to cancel. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
execution_id | string | Required | Execution ID of the cancelled run. |
status | string | Required | Updated status ("error"). |
message | string | Required | Human-readable cancellation message. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/test-runs/exec-a1b2c3d4/cancelResponse example
{
"execution_id": "exec-a1b2c3d4",
"status": "error",
"message": "Run cancelled"
}External Jobs
Create job-scoped automation entry points, inspect external-job health, and run retention cleanup.
GET/api/projects/{projectId}/external-jobsList external jobsUser token or API key
Returns external jobs for a project. Job-scoped API keys only receive their bound job.
# GET /api/projects/{projectId}/external-jobsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | Project UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | External job UUID. |
name | string | Required | Job display name. |
slug | string | Required | Project-unique slug. |
retention_policy | object | Required | Current retention policy. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/external-jobsResponse example
[
{
"id": "job-001",
"name": "Morning billing sync",
"slug": "morning-billing-sync"
}
]POST/api/projects/{projectId}/external-jobsCreate external jobUser token or API key
Creates a new external job. JWT callers receive the initial plaintext API key once; API-key-authenticated callers receive `api_key: null`.
# POST /api/projects/{projectId}/external-jobsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | Project UUID. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Job display name. |
description | string | Optional | Optional description. |
default_service | string | Optional | Default service for future runs. |
default_environment | string | Optional | Default environment for future runs. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
job | object | Required | Created external job. |
api_key | object | null | Required | Initial API key metadata, or `null` for API-key-authenticated callers. |
api_key.key | string | Optional | Plaintext key shown only once for JWT callers. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" -H "Content-Type: application/json" -d '{"name":"Morning billing sync"}' http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/external-jobsResponse example
{
"job": {
"id": "job-001",
"name": "Morning billing sync"
},
"api_key": {
"key": "bo_live_abc123"
}
}GET/api/external-jobs/{externalJobId}Get external jobUser token or API key
Returns the full configuration for one external job.
# GET /api/external-jobs/{externalJobId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | External job UUID. |
project_id | string | Required | Owning project UUID. |
retention_policy | object | Required | Retention policy. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/external-jobs/job-001Response example
[
{
"id": "job-001",
"project_id": "proj-001"
}
]PUT/api/external-jobs/{externalJobId}Update external jobUser token or API key
Updates retention and default metadata for an external job.
# PUT /api/external-jobs/{externalJobId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
retention_policy | object | Optional | Updated retention policy. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Updated job UUID. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer YOUR_JWT_TOKEN" -H "Content-Type: application/json" -d '{"retention_policy":{"events_days":7,"metrics_days":30,"attachments_days":7,"keep_run_summary_forever":true}}' http://localhost:8080/api/external-jobs/job-001Response example
[
{
"id": "job-001"
}
]GET/api/external-jobs/{externalJobId}/statsGet external job statsUser token or API key
Returns summary metrics such as total runs, pass rate, and average duration.
# GET /api/external-jobs/{externalJobId}/statsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
total_runs | number | Required | Total runs for this job. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/external-jobs/job-001/statsResponse example
{
"total_runs": 12,
"pass_rate": 91.7,
"avg_duration_ms": 18420
}GET/api/external-jobs/{externalJobId}/metricsList external job metricsUser token or API key
Returns metric points for one external job with optional name and time filters.
# GET /api/external-jobs/{externalJobId}/metricsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | Metric name to filter by. |
from | string | Optional | Inclusive ISO 8601 lower bound for event_at. |
to | string | Optional | Inclusive ISO 8601 upper bound for event_at. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Metric name. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/external-jobs/job-001/metrics?name=invoices.processed"Response example
[
{
"name": "invoices.processed",
"value": 124
}
]GET/api/external-jobs/{externalJobId}/runsList external job runsUser token or API key
Returns paginated runs for one external job.
# GET /api/external-jobs/{externalJobId}/runsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | number | Optional | Maximum number of runs to return. (Default: 100) |
offset | number | Optional | Pagination offset. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | Run[] | Required | Paginated run data. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/external-jobs/job-001/runs?limit=5&offset=0"Response example
{
"data": [
{
"id": "run-001",
"display_name": "Morning billing sync"
}
],
"total": 1,
"limit": 5,
"offset": 0
}GET/api/external-jobs/{externalJobId}/cleanup-statusGet cleanup statusUser token or API key
Returns the most recent retention cleanup run for an external job.
# GET /api/external-jobs/{externalJobId}/cleanup-statusPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
cleanup | object | Optional | Latest cleanup record, if any. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/external-jobs/job-001/cleanup-statusResponse example
{
"cleanup": {
"status": "completed",
"deleted_logs": 120
}
}POST/api/external-jobs/{externalJobId}/cleanupRun retention cleanupUser token or API key
Triggers retention cleanup for one external job immediately.
# POST /api/external-jobs/{externalJobId}/cleanupPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
externalJobId | string | Required | External job UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
cleanup.status | string | Required | Cleanup result status. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/external-jobs/job-001/cleanupResponse example
{
"cleanup": {
"status": "completed",
"deleted_storage_objects": 1
}
}External Ingest
Use a scoped external job key to start runs, stream telemetry, upload attachments, and finish runs from any system.
POST/api/ingest/runsStart external runUser token or API key
Creates a new external run or returns the existing run when the same external_run_id is retried.
# POST /api/ingest/runsRequest body
| Field | Type / status | Required | Description |
|---|---|---|---|
external_job_id | string | Optional | External job UUID. Optional when the API key is already job-scoped. |
external_run_id | string | Optional | Caller-defined idempotency key for the run. |
display_name | string | Optional | Human-readable run title. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
execution_id | string | Required | Created or reused execution ID. |
duplicate | boolean | Required | Whether this call reused an existing run. |
progress_sequence | number | Required | Latest accepted producer sequence, or zero for a new run. |
progress_revision | number | Required | Latest persisted server revision, or zero for a new run. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"external_job_id":"job-001","external_run_id":"cron-2026-04-08T09:00:00Z","display_name":"Morning billing sync"}' http://localhost:8080/api/ingest/runsResponse example
{
"execution_id": "run-001",
"status": "running",
"duplicate": false,
"progress_sequence": 0,
"progress_revision": 0
}POST/api/ingest/runs/{runId}/logsAppend log eventUser token or API key
Stores one log event for a running external execution.
# POST /api/ingest/runs/{runId}/logsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
event_id | string | Optional | Idempotency key for this log. |
message | string | Required | Log message. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
accepted | boolean | Required | Whether the event was accepted. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"event_id":"log-001","message":"Fetched 124 invoices"}' http://localhost:8080/api/ingest/runs/run-001/logsResponse example
{
"accepted": true,
"duplicate": false
}POST/api/ingest/runs/{runId}/stepsAppend step eventUser token or API key
Stores one step event for the execution timeline.
# POST /api/ingest/runs/{runId}/stepsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
event_id | string | Optional | Idempotency key for this step event. |
event | string | Required | Event name such as step:start or step:end. |
title | string | Optional | Display title for the step. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
accepted | boolean | Required | Whether the event was accepted. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"event":"step:start","title":"Fetch invoices"}' http://localhost:8080/api/ingest/runs/run-001/stepsResponse example
{
"accepted": true,
"duplicate": false
}POST/api/ingest/runs/{runId}/metricsAppend metric pointUser token or API key
Stores one metric sample for the execution.
# POST /api/ingest/runs/{runId}/metricsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
event_id | string | Optional | Idempotency key for this metric sample. |
name | string | Required | Metric name. |
kind | string | Required | gauge, count, rate, or duration. |
value | number | Required | Metric value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
accepted | boolean | Required | Whether the metric was accepted. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"name":"invoices.processed","kind":"count","value":124}' http://localhost:8080/api/ingest/runs/run-001/metricsResponse example
{
"accepted": true,
"duplicate": false
}POST/api/ingest/runs/{runId}/progressReport run progressUser token or API key
Atomically stores count, percent-only, or indeterminate progress. Exact event retries return the original canonical snapshot.
# POST /api/ingest/runs/{runId}/progressPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
event_id | string | Required | Stable idempotency key for this progress report. |
sequence | number | Required | Positive increasing producer sequence. |
current | number | Optional | Current count; provide with total only. |
total | number | Optional | Positive total count; provide with current only. |
percent | number | Optional | Percent-only progress from 0 through 100. |
stage | string | Optional | Current stage, up to 128 characters. |
message | string | Optional | Progress message, up to 1024 characters. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
accepted | boolean | Required | Whether the report was accepted. |
duplicate | boolean | Required | Whether this was an exact retry. |
progress | ProgressSnapshot | Required | Canonical persisted snapshot with server revision. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"event_id":"progress-001","sequence":1,"current":62,"total":100,"stage":"processing"}' http://localhost:8080/api/ingest/runs/run-001/progressResponse example
{
"accepted": true,
"duplicate": false,
"progress": {
"execution_id": "run-001",
"event_id": "progress-001",
"sequence": 1,
"revision": 1,
"mode": "count",
"current": 62,
"total": 100,
"percent": 62,
"stage": "processing",
"message": "Processed 62 invoices",
"reported_at": "2026-04-08T09:00:32Z",
"updated_at": "2026-04-08T09:00:32.100Z"
}
}POST/api/ingest/runs/{runId}/attachmentsUpload attachmentUser token or API key
Streams one supported attachment into the private execution-attachments bucket and stores metadata on the run.
# POST /api/ingest/runs/{runId}/attachmentsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
file | multipart file | Required | Supported file to upload. |
event_id | string | Optional | Idempotency key for this attachment. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
attachment_id | string | Required | Created attachment UUID. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -F "[email protected];type=application/json" -F "event_id=att-001" http://localhost:8080/api/ingest/runs/run-001/attachmentsResponse example
{
"attachment_id": "att-001",
"file_name": "report.json"
}POST/api/ingest/runs/{runId}/heartbeatUpdate heartbeatUser token or API key
Updates the last heartbeat timestamp for a running external execution.
# POST /api/ingest/runs/{runId}/heartbeatPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
heartbeat_at | string | Optional | Optional ISO 8601 heartbeat timestamp. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
last_heartbeat_at | string | Required | Stored heartbeat timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"heartbeat_at":"2026-04-08T09:05:00Z"}' http://localhost:8080/api/ingest/runs/run-001/heartbeatResponse example
{
"execution_id": "run-001",
"last_heartbeat_at": "2026-04-08T09:05:00Z"
}POST/api/ingest/runs/{runId}/completeComplete external runUser token or API key
Marks the external execution as passed and stores a final summary.
# POST /api/ingest/runs/{runId}/completePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
finished_at | string | Optional | ISO 8601 finish timestamp. |
duration_ms | number | Optional | Execution duration in milliseconds. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
status | string | Required | Final execution status. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"finished_at":"2026-04-08T09:10:00Z","duration_ms":600000}' http://localhost:8080/api/ingest/runs/run-001/completeResponse example
{
"execution_id": "run-001",
"status": "passed"
}POST/api/ingest/runs/{runId}/failFail external runUser token or API key
Marks the external execution as failed.
# POST /api/ingest/runs/{runId}/failPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
runId | string | Required | Execution UUID returned by start run. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
error | string | Required | Error message describing the failure. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
status | string | Required | Final execution status. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -d '{"error":"Unable to process invoices"}' http://localhost:8080/api/ingest/runs/run-001/failResponse example
{
"execution_id": "run-001",
"status": "failed"
}GET/api/test-runs/{executionId}/metricsGet run metricsUser token or API key
Returns paginated metric points for one execution.
# GET /api/test-runs/{executionId}/metricsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution UUID. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | number | Optional | Maximum records to return. (Default: 100) |
offset | number | Optional | Pagination offset. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | Metric[] | Required | Paginated metric data. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/test-runs/run-001/metrics?limit=100&offset=0"Response example
{
"data": [
{
"name": "invoices.processed",
"value": 124,
"unit": "items"
}
],
"total": 1,
"limit": 100,
"offset": 0
}GET/api/test-runs/{executionId}/progressGet run progress historyUser token or API key
Returns paginated canonical progress snapshots ordered by server revision.
# GET /api/test-runs/{executionId}/progressPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution UUID. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | number | Optional | Maximum records to return. (Default: 50) |
offset | number | Optional | Pagination offset. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | ProgressSnapshot[] | Required | Canonical progress history. |
total | number | Required | Total retained progress events. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/test-runs/run-001/progress?limit=50&offset=0"Response example
{
"data": [
{
"execution_id": "run-001",
"event_id": "progress-001",
"sequence": 1,
"revision": 1,
"mode": "percent",
"current": null,
"total": null,
"percent": 72.5,
"stage": "uploading",
"message": null,
"reported_at": "2026-04-08T09:05:32Z",
"updated_at": "2026-04-08T09:05:32.100Z"
}
],
"total": 1,
"limit": 50,
"offset": 0
}GET/api/test-runs/{executionId}/attachmentsGet run attachmentsUser token or API key
Returns paginated attachment metadata for one execution.
# GET /api/test-runs/{executionId}/attachmentsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution UUID. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | number | Optional | Maximum records to return. (Default: 50) |
offset | number | Optional | Pagination offset. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | Attachment[] | Required | Paginated attachment data. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/test-runs/run-001/attachments?limit=50&offset=0"Response example
{
"data": [
{
"file_name": "report.json",
"content_type": "application/json",
"size_bytes": 17
}
],
"total": 1,
"limit": 50,
"offset": 0
}GET/api/test-runs/{executionId}/attachments/{attachmentId}/downloadGet attachment download URLUser token or API key
Returns a short-lived signed URL for downloading a private run attachment.
# GET /api/test-runs/{executionId}/attachments/{attachmentId}/downloadPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
executionId | string | Required | Execution UUID. |
attachmentId | string | Required | Attachment UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
attachment_id | string | Required | Attachment UUID. |
file_name | string | Required | Attachment file name. |
content_type | string | Required | Attachment content type. |
download_url | string | Required | Short-lived signed download URL. |
expires_at | string | Required | ISO 8601 expiration timestamp for the signed URL. |
expires_in_seconds | number | Required | Signed URL lifetime in seconds. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/test-runs/run-001/attachments/att-001/downloadResponse example
{
"attachment_id": "att-001",
"file_name": "report.json",
"content_type": "application/json",
"download_url": "http://127.0.0.1:54321/storage/v1/object/sign/execution-attachments/project/...",
"expires_at": "2026-04-08T09:06:00Z",
"expires_in_seconds": 300
}Wiki
Manage the independent project wiki. Dual-auth routes accept a project member JWT or a project-bound API key with all documented wiki scopes. API keys never gain archive, unarchive, attachment-delete, or archive-import administration rights. Authentication middleware may return plain-text 401/403 errors before a coded wiki response.
GET/api/projects/{projectId}/wiki/pagesList or search wiki pagesUser token or API key
Returns bounded page summaries without Markdown bodies, plus the normalized search options and caller capabilities. Results come from the project-scoped full-text search and can include active, archived, or all pages.
# GET /api/projects/{projectId}/wiki/pagesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
q | string | Optional | Trimmed full-text query, at most 500 Unicode characters. |
archive | active | archived | all | Optional | Archive-state filter. (Default: active) |
limit | integer | Optional | Maximum page summaries, from 1 through 100. (Default: 50) |
offset | integer | Optional | Zero-based result offset, from 0 through 10000. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
pages | WikiPageSummary[] | Required | Summaries with id, project_id, title, slug, filename, revision, archived_at, updated_at, and numeric rank; body_markdown is intentionally omitted. |
q | string | Required | Normalized query. |
archive | active | archived | all | Required | Applied archive filter. |
limit | integer | Required | Applied limit. |
offset | integer | Required | Applied offset. |
capabilities.can_create | boolean | Required | Whether this principal can create pages. |
capabilities.can_export | boolean | Required | Always true after project read authorization. |
capabilities.can_import | boolean | Required | True only for an authorized JWT project administrator. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
invalid_wiki_search | 400 | Optional | q, archive, limit, or offset is outside the accepted contract. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages?q=incident&archive=active&limit=50&offset=0"Response example
{
"pages": [
{
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"title": "Incident response",
"slug": "incident-response",
"filename": "runbooks/incident-response.md",
"revision": 3,
"archived_at": null,
"updated_at": "2026-08-16T10:30:00Z",
"rank": 0.42
}
],
"q": "incident",
"archive": "active",
"limit": 50,
"offset": 0,
"capabilities": {
"can_create": true,
"can_export": true,
"can_import": false
}
}POST/api/projects/{projectId}/wiki/pagesCreate a wiki pageUser token or API key
Creates revision 1 transactionally. JWT users need project mutation access; API keys need wiki:write and Idempotency-Key. Slug defaults from title, filename defaults from slug, and Markdown defaults to empty. JSON is capped at 8 MiB and unknown fields are rejected.
# POST /api/projects/{projectId}/wiki/pagesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Optional | Required for API-key commands and optional for JWT commands. After trimming, must be 1-255 Unicode characters. An exact retry by the same actor replays the saved result; reuse for different input returns idempotency_mismatch. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
title | string | Required | Title normalized by trimming; final length is 1-300 characters. |
slug | string | Optional | Source for a lowercase kebab-case slug, at most 200 characters. Defaults to title. |
filename | string | Optional | Source for a lowercase relative .md path, 3-512 characters. Defaults to {slug}.md. |
body_markdown | string | Optional | UTF-8 Markdown; canonical result is at most 1 MiB. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
outcome | applied | noop | Required | Whether this command changed the page. |
code | string | Required | Stable success/no-op code for this command. |
page.id | UUID | Required | Page UUID. |
page.project_id | UUID | Required | Owning project UUID. |
page.title | string | Required | Trimmed title, 1-300 characters. |
page.slug | string | Required | Lowercase kebab-case slug, at most 200 characters. |
page.normalized_slug | string | Required | Stored normalized slug used for project uniqueness. |
page.filename | string | Required | Lowercase project-relative .md filename, 3-512 characters. |
page.normalized_filename | string | Required | Stored normalized filename used for project uniqueness. |
page.body_markdown | string | Required | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
page.revision | integer | Required | Current positive revision, up to 9007199254740991. |
page.archived_at | ISO 8601 string | null | Required | Archive timestamp, or null while active. |
page.created_actor_kind | user | api_key | system | Required | Principal kind that created the page. |
page.created_actor_user_id | UUID | Optional | Creating user ID when actor_kind is user. |
page.created_actor_api_key_id | UUID | Optional | Creating API key ID when actor_kind is api_key. |
page.created_actor_display | string | null | Required | Stored display identity for the creating actor. |
page.updated_actor_kind | user | api_key | system | Required | Principal kind that last changed the page. |
page.updated_actor_user_id | UUID | Optional | Last updating user ID when actor_kind is user. |
page.updated_actor_api_key_id | UUID | Optional | Last updating API key ID when actor_kind is api_key. |
page.updated_actor_display | string | null | Required | Stored display identity for the last updating actor. |
page.created_at | ISO 8601 string | Required | Creation timestamp. |
page.updated_at | ISO 8601 string | Required | Last update timestamp. |
audit_event_id | UUID | null | Required | Append-only audit event ID, or null for a no-op. |
source_revision | integer | Optional | Source revision on a restore result. |
replayed | boolean | Required | True when an exact Idempotency-Key retry replayed this result. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
invalid_json_body | 400 | Optional | The body is not one JSON value, contains unknown fields, or has invalid field types. |
request_body_too_large | 413 | Optional | The JSON request exceeds the route's byte limit. |
invalid_idempotency_key | 400 | Optional | An API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters. |
idempotency_mismatch | 409 | Optional | The same actor already used this Idempotency-Key for different canonical command input. |
invalid_title | 400 | Optional | The normalized title is empty or longer than 300 characters. |
invalid_slug | 400 | Optional | The normalized slug is empty, longer than 200 characters, or cannot match lowercase kebab-case. |
invalid_filename | 400 | Optional | The normalized Markdown filename is invalid, hidden, non-canonical, or longer than 512 characters. |
markdown_too_large | 413 | Optional | Canonical UTF-8 Markdown exceeds 1 MiB. |
wiki_slug_conflict | 409 | Optional | Another project wiki page already uses the normalized slug. |
wiki_filename_conflict | 409 | Optional | Another project wiki page already uses the normalized filename. |
revision_exhausted | 409 | Optional | The page reached revision 9007199254740991. |
wiki_attachment_reference_not_found | 409 | Optional | Markdown references an attachment that does not exist on this project page. |
invalid_create_input | 400 | Optional | title is missing or a request field has an invalid type. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -H "Idempotency-Key: create-incident-response-v1" -d '{"title":"Incident response","slug":"incident-response","filename":"runbooks/incident-response.md","body_markdown":"# Incident response\n"}' http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pagesResponse example
{
"outcome": "applied",
"code": "wiki_page_created",
"page": {
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"title": "Incident response",
"slug": "incident-response",
"normalized_slug": "incident-response",
"filename": "runbooks/incident-response.md",
"normalized_filename": "runbooks/incident-response.md",
"body_markdown": "# Incident response\n",
"revision": 1,
"archived_at": null,
"created_actor_kind": "api_key",
"created_actor_api_key_id": "d2600000-0000-0000-0000-000000000401",
"created_actor_display": "d2600000-0000-0000-0000-000000000401",
"updated_actor_kind": "api_key",
"updated_actor_api_key_id": "d2600000-0000-0000-0000-000000000401",
"updated_actor_display": "d2600000-0000-0000-0000-000000000401",
"created_at": "2026-08-16T10:00:00Z",
"updated_at": "2026-08-16T10:00:00Z"
},
"audit_event_id": "d2600000-0000-0000-0000-000000000501",
"replayed": false
}GET/api/projects/{projectId}/wiki/pages/{pageId}Get a wiki pageUser token or API key
Returns the current page head, including canonical Markdown, actor metadata, archive state, and capabilities calculated for the principal.
# GET /api/projects/{projectId}/wiki/pages/{pageId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
page.id | UUID | Required | Page UUID. |
page.project_id | UUID | Required | Owning project UUID. |
page.title | string | Required | Trimmed title, 1-300 characters. |
page.slug | string | Required | Lowercase kebab-case slug, at most 200 characters. |
page.normalized_slug | string | Required | Stored normalized slug used for project uniqueness. |
page.filename | string | Required | Lowercase project-relative .md filename, 3-512 characters. |
page.normalized_filename | string | Required | Stored normalized filename used for project uniqueness. |
page.body_markdown | string | Required | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
page.revision | integer | Required | Current positive revision, up to 9007199254740991. |
page.archived_at | ISO 8601 string | null | Required | Archive timestamp, or null while active. |
page.created_actor_kind | user | api_key | system | Required | Principal kind that created the page. |
page.created_actor_user_id | UUID | Optional | Creating user ID when actor_kind is user. |
page.created_actor_api_key_id | UUID | Optional | Creating API key ID when actor_kind is api_key. |
page.created_actor_display | string | null | Required | Stored display identity for the creating actor. |
page.updated_actor_kind | user | api_key | system | Required | Principal kind that last changed the page. |
page.updated_actor_user_id | UUID | Optional | Last updating user ID when actor_kind is user. |
page.updated_actor_api_key_id | UUID | Optional | Last updating API key ID when actor_kind is api_key. |
page.updated_actor_display | string | null | Required | Stored display identity for the last updating actor. |
page.created_at | ISO 8601 string | Required | Creation timestamp. |
page.updated_at | ISO 8601 string | Required | Last update timestamp. |
capabilities.can_read | boolean | Required | True after read authorization. |
capabilities.can_export | boolean | Required | True after read authorization. |
capabilities.can_update | boolean | Required | True for a mutable active page and a mutation-capable principal. |
capabilities.can_restore | boolean | Required | True for a mutation-capable principal, including on archived pages. |
capabilities.can_archive | boolean | Required | True only for a JWT administrator and an active page. |
capabilities.can_unarchive | boolean | Required | True only for a JWT administrator and an archived page. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
Content types: application/json → application/json
cURL example
curl -i -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201Response example
{
"page": {
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"title": "Incident response",
"slug": "incident-response",
"normalized_slug": "incident-response",
"filename": "runbooks/incident-response.md",
"normalized_filename": "runbooks/incident-response.md",
"body_markdown": "# Incident response\n",
"revision": 1,
"archived_at": null,
"created_actor_kind": "user",
"created_actor_user_id": "d2600000-0000-0000-0000-000000000301",
"created_actor_display": "d2600000-0000-0000-0000-000000000301",
"updated_actor_kind": "user",
"updated_actor_user_id": "d2600000-0000-0000-0000-000000000301",
"updated_actor_display": "d2600000-0000-0000-0000-000000000301",
"created_at": "2026-08-16T10:00:00Z",
"updated_at": "2026-08-16T10:00:00Z"
},
"capabilities": {
"can_read": true,
"can_export": true,
"can_update": true,
"can_restore": true,
"can_archive": true,
"can_unarchive": false
}
}PUT/api/projects/{projectId}/wiki/pages/{pageId}Update a wiki pageUser token or API key
Applies a partial page-head update under exact ETag concurrency. API keys need both wiki:read and wiki:write because successful and no-op responses include the current page body. Omitted fields are unchanged; an empty or equivalent change is a wiki_page_unchanged no-op. Archived pages cannot be updated. JSON is capped at 8 MiB and unknown fields are rejected.
# PUT /api/projects/{projectId}/wiki/pages/{pageId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
If-Match | strong ETag | Required | Exact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected. |
Idempotency-Key | string | Optional | Required for API-key commands and optional for JWT commands. After trimming, must be 1-255 Unicode characters. An exact retry by the same actor replays the saved result; reuse for different input returns idempotency_mismatch. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
title | string | Optional | Trimmed title, 1-300 characters. |
slug | string | Optional | Lowercase kebab-case slug, at most 200 characters. |
filename | string | Optional | Lowercase project-relative .md filename, 3-512 characters. |
body_markdown | string | Optional | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
outcome | applied | noop | Required | Whether this command changed the page. |
code | string | Required | Stable success/no-op code for this command. |
page.id | UUID | Required | Page UUID. |
page.project_id | UUID | Required | Owning project UUID. |
page.title | string | Required | Trimmed title, 1-300 characters. |
page.slug | string | Required | Lowercase kebab-case slug, at most 200 characters. |
page.normalized_slug | string | Required | Stored normalized slug used for project uniqueness. |
page.filename | string | Required | Lowercase project-relative .md filename, 3-512 characters. |
page.normalized_filename | string | Required | Stored normalized filename used for project uniqueness. |
page.body_markdown | string | Required | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
page.revision | integer | Required | Current positive revision, up to 9007199254740991. |
page.archived_at | ISO 8601 string | null | Required | Archive timestamp, or null while active. |
page.created_actor_kind | user | api_key | system | Required | Principal kind that created the page. |
page.created_actor_user_id | UUID | Optional | Creating user ID when actor_kind is user. |
page.created_actor_api_key_id | UUID | Optional | Creating API key ID when actor_kind is api_key. |
page.created_actor_display | string | null | Required | Stored display identity for the creating actor. |
page.updated_actor_kind | user | api_key | system | Required | Principal kind that last changed the page. |
page.updated_actor_user_id | UUID | Optional | Last updating user ID when actor_kind is user. |
page.updated_actor_api_key_id | UUID | Optional | Last updating API key ID when actor_kind is api_key. |
page.updated_actor_display | string | null | Required | Stored display identity for the last updating actor. |
page.created_at | ISO 8601 string | Required | Creation timestamp. |
page.updated_at | ISO 8601 string | Required | Last update timestamp. |
audit_event_id | UUID | null | Required | Append-only audit event ID, or null for a no-op. |
source_revision | integer | Optional | Source revision on a restore result. |
replayed | boolean | Required | True when an exact Idempotency-Key retry replayed this result. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
invalid_json_body | 400 | Optional | The body is not one JSON value, contains unknown fields, or has invalid field types. |
request_body_too_large | 413 | Optional | The JSON request exceeds the route's byte limit. |
invalid_idempotency_key | 400 | Optional | An API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters. |
idempotency_mismatch | 409 | Optional | The same actor already used this Idempotency-Key for different canonical command input. |
invalid_title | 400 | Optional | The normalized title is empty or longer than 300 characters. |
invalid_slug | 400 | Optional | The normalized slug is empty, longer than 200 characters, or cannot match lowercase kebab-case. |
invalid_filename | 400 | Optional | The normalized Markdown filename is invalid, hidden, non-canonical, or longer than 512 characters. |
markdown_too_large | 413 | Optional | Canonical UTF-8 Markdown exceeds 1 MiB. |
wiki_slug_conflict | 409 | Optional | Another project wiki page already uses the normalized slug. |
wiki_filename_conflict | 409 | Optional | Another project wiki page already uses the normalized filename. |
revision_exhausted | 409 | Optional | The page reached revision 9007199254740991. |
wiki_attachment_reference_not_found | 409 | Optional | Markdown references an attachment that does not exist on this project page. |
if_match_required | 428 | Optional | If-Match is required for this page command. |
invalid_if_match | 400 | Optional | If-Match is not the exact strong ETag for this page. |
revision_conflict | 412 | Optional | The page has changed. The error details include expected_revision, current_revision, and current_etag; the response ETag is current. |
invalid_update_changes | 400 | Optional | A change is unknown, null, or not a string. |
wiki_page_archived | 409 | Optional | Archived pages cannot be updated. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -H 'If-Match: "wiki-page:d2600000-0000-0000-0000-000000000201:rev:1"' -H "Idempotency-Key: update-incident-response-v2" -d '{"title":"Incident response plan"}' http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201Response example
{
"outcome": "applied",
"code": "wiki_page_updated",
"page": {
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"revision": 2,
"title": "Incident response plan"
},
"audit_event_id": "d2600000-0000-0000-0000-000000000502",
"replayed": false
}POST/api/projects/{projectId}/wiki/pages/{pageId}/archiveArchive a wiki pageUser token
JWT-admin-only soft archive under exact ETag concurrency. API keys are rejected regardless of scope. An already archived page returns a wiki_page_already_archived no-op. The body may be omitted or be exactly {}; non-empty JSON is rejected and the body limit is 1 KiB.
# POST /api/projects/{projectId}/wiki/pages/{pageId}/archivePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
If-Match | strong ETag | Required | Exact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected. |
Idempotency-Key | string | Optional | Required for API-key commands and optional for JWT commands. After trimming, must be 1-255 Unicode characters. An exact retry by the same actor replays the saved result; reuse for different input returns idempotency_mismatch. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
outcome | applied | noop | Required | Whether this command changed the page. |
code | string | Required | Stable success/no-op code for this command. |
page.id | UUID | Required | Page UUID. |
page.project_id | UUID | Required | Owning project UUID. |
page.title | string | Required | Trimmed title, 1-300 characters. |
page.slug | string | Required | Lowercase kebab-case slug, at most 200 characters. |
page.normalized_slug | string | Required | Stored normalized slug used for project uniqueness. |
page.filename | string | Required | Lowercase project-relative .md filename, 3-512 characters. |
page.normalized_filename | string | Required | Stored normalized filename used for project uniqueness. |
page.body_markdown | string | Required | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
page.revision | integer | Required | Current positive revision, up to 9007199254740991. |
page.archived_at | ISO 8601 string | null | Required | Archive timestamp, or null while active. |
page.created_actor_kind | user | api_key | system | Required | Principal kind that created the page. |
page.created_actor_user_id | UUID | Optional | Creating user ID when actor_kind is user. |
page.created_actor_api_key_id | UUID | Optional | Creating API key ID when actor_kind is api_key. |
page.created_actor_display | string | null | Required | Stored display identity for the creating actor. |
page.updated_actor_kind | user | api_key | system | Required | Principal kind that last changed the page. |
page.updated_actor_user_id | UUID | Optional | Last updating user ID when actor_kind is user. |
page.updated_actor_api_key_id | UUID | Optional | Last updating API key ID when actor_kind is api_key. |
page.updated_actor_display | string | null | Required | Stored display identity for the last updating actor. |
page.created_at | ISO 8601 string | Required | Creation timestamp. |
page.updated_at | ISO 8601 string | Required | Last update timestamp. |
audit_event_id | UUID | null | Required | Append-only audit event ID, or null for a no-op. |
source_revision | integer | Optional | Source revision on a restore result. |
replayed | boolean | Required | True when an exact Idempotency-Key retry replayed this result. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_admin_required | 403 | Optional | The operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it. |
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
invalid_json_body | 400 | Optional | The body is not one JSON value, contains unknown fields, or has invalid field types. |
request_body_too_large | 413 | Optional | The JSON request exceeds the route's byte limit. |
invalid_idempotency_key | 400 | Optional | An API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters. |
idempotency_mismatch | 409 | Optional | The same actor already used this Idempotency-Key for different canonical command input. |
if_match_required | 428 | Optional | If-Match is required for this page command. |
invalid_if_match | 400 | Optional | If-Match is not the exact strong ETag for this page. |
revision_conflict | 412 | Optional | The page has changed. The error details include expected_revision, current_revision, and current_etag; the response ETag is current. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" -H "Content-Type: application/json" -H 'If-Match: "wiki-page:d2600000-0000-0000-0000-000000000201:rev:1"' -H "Idempotency-Key: archive-incident-response-v1" -d '{}' http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/archiveResponse example
{
"outcome": "applied",
"code": "wiki_page_archived",
"page": {
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"revision": 2,
"archived_at": "2026-08-16T11:00:00Z"
},
"audit_event_id": "d2600000-0000-0000-0000-000000000503",
"replayed": false
}POST/api/projects/{projectId}/wiki/pages/{pageId}/unarchiveUnarchive a wiki pageUser token
JWT-admin-only reactivation under exact ETag concurrency. API keys are rejected regardless of scope. An already active page returns a wiki_page_already_active no-op. The body may be omitted or be exactly {}; non-empty JSON is rejected and the body limit is 1 KiB.
# POST /api/projects/{projectId}/wiki/pages/{pageId}/unarchivePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
If-Match | strong ETag | Required | Exact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected. |
Idempotency-Key | string | Optional | Required for API-key commands and optional for JWT commands. After trimming, must be 1-255 Unicode characters. An exact retry by the same actor replays the saved result; reuse for different input returns idempotency_mismatch. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
outcome | applied | noop | Required | Whether this command changed the page. |
code | string | Required | Stable success/no-op code for this command. |
page.id | UUID | Required | Page UUID. |
page.project_id | UUID | Required | Owning project UUID. |
page.title | string | Required | Trimmed title, 1-300 characters. |
page.slug | string | Required | Lowercase kebab-case slug, at most 200 characters. |
page.normalized_slug | string | Required | Stored normalized slug used for project uniqueness. |
page.filename | string | Required | Lowercase project-relative .md filename, 3-512 characters. |
page.normalized_filename | string | Required | Stored normalized filename used for project uniqueness. |
page.body_markdown | string | Required | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
page.revision | integer | Required | Current positive revision, up to 9007199254740991. |
page.archived_at | ISO 8601 string | null | Required | Archive timestamp, or null while active. |
page.created_actor_kind | user | api_key | system | Required | Principal kind that created the page. |
page.created_actor_user_id | UUID | Optional | Creating user ID when actor_kind is user. |
page.created_actor_api_key_id | UUID | Optional | Creating API key ID when actor_kind is api_key. |
page.created_actor_display | string | null | Required | Stored display identity for the creating actor. |
page.updated_actor_kind | user | api_key | system | Required | Principal kind that last changed the page. |
page.updated_actor_user_id | UUID | Optional | Last updating user ID when actor_kind is user. |
page.updated_actor_api_key_id | UUID | Optional | Last updating API key ID when actor_kind is api_key. |
page.updated_actor_display | string | null | Required | Stored display identity for the last updating actor. |
page.created_at | ISO 8601 string | Required | Creation timestamp. |
page.updated_at | ISO 8601 string | Required | Last update timestamp. |
audit_event_id | UUID | null | Required | Append-only audit event ID, or null for a no-op. |
source_revision | integer | Optional | Source revision on a restore result. |
replayed | boolean | Required | True when an exact Idempotency-Key retry replayed this result. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_admin_required | 403 | Optional | The operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it. |
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
invalid_json_body | 400 | Optional | The body is not one JSON value, contains unknown fields, or has invalid field types. |
request_body_too_large | 413 | Optional | The JSON request exceeds the route's byte limit. |
invalid_idempotency_key | 400 | Optional | An API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters. |
idempotency_mismatch | 409 | Optional | The same actor already used this Idempotency-Key for different canonical command input. |
if_match_required | 428 | Optional | If-Match is required for this page command. |
invalid_if_match | 400 | Optional | If-Match is not the exact strong ETag for this page. |
revision_conflict | 412 | Optional | The page has changed. The error details include expected_revision, current_revision, and current_etag; the response ETag is current. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" -H "Content-Type: application/json" -H 'If-Match: "wiki-page:d2600000-0000-0000-0000-000000000201:rev:2"' -d '{}' http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/unarchiveResponse example
{
"outcome": "applied",
"code": "wiki_page_unarchived",
"page": {
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"revision": 3,
"archived_at": null
},
"audit_event_id": "d2600000-0000-0000-0000-000000000504",
"replayed": false
}GET/api/projects/{projectId}/wiki/pages/{pageId}/revisionsList wiki page revisionsUser token or API key
Returns newest-first immutable revision metadata without Markdown bodies. The page must still exist, including when archived.
# GET /api/projects/{projectId}/wiki/pages/{pageId}/revisionsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
limit | integer | Optional | Maximum revision summaries, from 1 through 100. (Default: 50) |
offset | integer | Optional | Zero-based revision offset, from 0 through 10000. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
revisions | WikiRevisionSummary[] | Required | Rows with id, project_id, page_id, revision, revision_kind, restored_from_revision, title, slug, filename, archived_at, actor fields, and recorded_at; body_markdown is omitted. |
page_id | UUID | Required | Requested page UUID. |
limit | integer | Required | Applied limit. |
offset | integer | Required | Applied offset. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
invalid_revision_query | 400 | Optional | limit or offset is outside the accepted range. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" "http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/revisions?limit=50&offset=0"Response example
{
"revisions": [
{
"id": "d2600000-0000-0000-0000-000000000601",
"project_id": "b0000000-0000-0000-0000-000000000001",
"page_id": "d2600000-0000-0000-0000-000000000201",
"revision": 2,
"revision_kind": "updated",
"restored_from_revision": null,
"title": "Incident response plan",
"slug": "incident-response",
"filename": "runbooks/incident-response.md",
"archived_at": null,
"actor_kind": "api_key",
"actor_api_key_id": "d2600000-0000-0000-0000-000000000401",
"actor_display": "d2600000-0000-0000-0000-000000000401",
"recorded_at": "2026-08-16T10:30:00Z"
}
],
"page_id": "d2600000-0000-0000-0000-000000000201",
"limit": 50,
"offset": 0
}GET/api/projects/{projectId}/wiki/pages/{pageId}/revisions/{revision}Get a wiki page revisionUser token or API key
Returns one immutable revision snapshot, including its Markdown body. Revision is a canonical decimal integer from 1 through 9007199254740991; signs, leading zeroes, fractions, and out-of-range values are not accepted.
# GET /api/projects/{projectId}/wiki/pages/{pageId}/revisions/{revision}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
revision | integer | Required | Canonical positive revision number, without leading zeroes. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
revision | WikiRevision | Required | Immutable snapshot with id, project_id, page_id, revision, revision_kind, restored_from_revision, title, slug, filename, body_markdown, archived_at, actor fields, and recorded_at. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
wiki_revision_not_found | 404 | Optional | The revision path is invalid or that revision does not exist for this page. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/revisions/1Response example
{
"revision": {
"id": "d2600000-0000-0000-0000-000000000601",
"project_id": "b0000000-0000-0000-0000-000000000001",
"page_id": "d2600000-0000-0000-0000-000000000201",
"revision": 1,
"revision_kind": "created",
"restored_from_revision": null,
"title": "Incident response",
"slug": "incident-response",
"filename": "runbooks/incident-response.md",
"body_markdown": "# Incident response\n",
"archived_at": null,
"actor_kind": "user",
"actor_user_id": "d2600000-0000-0000-0000-000000000301",
"actor_display": "d2600000-0000-0000-0000-000000000301",
"recorded_at": "2026-08-16T10:00:00Z"
}
}POST/api/projects/{projectId}/wiki/pages/{pageId}/restoreRestore a wiki page revisionUser token or API key
Copies one immutable snapshot's title, slug, filename, and Markdown into a new page-head revision under exact ETag concurrency. API keys need both wiki:read and wiki:write because successful and no-op responses include the current page body. Restore is available on active or archived pages, but it does not change archived_at. An identical snapshot is a wiki_page_unchanged no-op. The JSON body limit is 1 KiB.
# POST /api/projects/{projectId}/wiki/pages/{pageId}/restorePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
If-Match | strong ETag | Required | Exact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected. |
Idempotency-Key | string | Optional | Required for API-key commands and optional for JWT commands. After trimming, must be 1-255 Unicode characters. An exact retry by the same actor replays the saved result; reuse for different input returns idempotency_mismatch. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
source_revision | integer | Required | Existing positive revision to copy, up to 9007199254740991. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
outcome | applied | noop | Required | Whether this command changed the page. |
code | string | Required | Stable success/no-op code for this command. |
page.id | UUID | Required | Page UUID. |
page.project_id | UUID | Required | Owning project UUID. |
page.title | string | Required | Trimmed title, 1-300 characters. |
page.slug | string | Required | Lowercase kebab-case slug, at most 200 characters. |
page.normalized_slug | string | Required | Stored normalized slug used for project uniqueness. |
page.filename | string | Required | Lowercase project-relative .md filename, 3-512 characters. |
page.normalized_filename | string | Required | Stored normalized filename used for project uniqueness. |
page.body_markdown | string | Required | Canonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF. |
page.revision | integer | Required | Current positive revision, up to 9007199254740991. |
page.archived_at | ISO 8601 string | null | Required | Archive timestamp, or null while active. |
page.created_actor_kind | user | api_key | system | Required | Principal kind that created the page. |
page.created_actor_user_id | UUID | Optional | Creating user ID when actor_kind is user. |
page.created_actor_api_key_id | UUID | Optional | Creating API key ID when actor_kind is api_key. |
page.created_actor_display | string | null | Required | Stored display identity for the creating actor. |
page.updated_actor_kind | user | api_key | system | Required | Principal kind that last changed the page. |
page.updated_actor_user_id | UUID | Optional | Last updating user ID when actor_kind is user. |
page.updated_actor_api_key_id | UUID | Optional | Last updating API key ID when actor_kind is api_key. |
page.updated_actor_display | string | null | Required | Stored display identity for the last updating actor. |
page.created_at | ISO 8601 string | Required | Creation timestamp. |
page.updated_at | ISO 8601 string | Required | Last update timestamp. |
audit_event_id | UUID | null | Required | Append-only audit event ID, or null for a no-op. |
source_revision | integer | Optional | Source revision on a restore result. |
replayed | boolean | Required | True when an exact Idempotency-Key retry replayed this result. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
invalid_json_body | 400 | Optional | The body is not one JSON value, contains unknown fields, or has invalid field types. |
request_body_too_large | 413 | Optional | The JSON request exceeds the route's byte limit. |
invalid_idempotency_key | 400 | Optional | An API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters. |
idempotency_mismatch | 409 | Optional | The same actor already used this Idempotency-Key for different canonical command input. |
if_match_required | 428 | Optional | If-Match is required for this page command. |
invalid_if_match | 400 | Optional | If-Match is not the exact strong ETag for this page. |
revision_conflict | 412 | Optional | The page has changed. The error details include expected_revision, current_revision, and current_etag; the response ETag is current. |
invalid_restore_input | 400 | Optional | source_revision is missing or not a canonical positive integer. |
wiki_revision_not_found | 404 | Optional | The source revision does not exist for this page. |
wiki_slug_conflict | 409 | Optional | Restoring the snapshot would collide with another page slug. |
wiki_filename_conflict | 409 | Optional | Restoring the snapshot would collide with another page filename. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -H "Content-Type: application/json" -H 'If-Match: "wiki-page:d2600000-0000-0000-0000-000000000201:rev:2"' -H "Idempotency-Key: restore-incident-response-r1" -d '{"source_revision":1}' http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/restoreResponse example
{
"outcome": "applied",
"code": "wiki_page_restored",
"page": {
"id": "d2600000-0000-0000-0000-000000000201",
"project_id": "b0000000-0000-0000-0000-000000000001",
"revision": 3,
"title": "Incident response"
},
"audit_event_id": "d2600000-0000-0000-0000-000000000505",
"source_revision": 1,
"replayed": false
}GET/api/projects/{projectId}/wiki/templatesList system wiki templatesUser token or API key
Returns the read-only seeded system templates ordered by template_key after project access is checked. There are no template mutation routes.
# GET /api/projects/{projectId}/wiki/templatesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
templates | WikiTemplate[] | Required | System templates with id, template_key, name, description, body_markdown, version, is_system, created_at, and updated_at. Template Markdown is at most 256 KiB. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/templatesResponse example
{
"templates": [
{
"id": "d2600000-0000-0000-0000-000000000701",
"template_key": "runbook",
"name": "Runbook",
"description": "Operational procedure with verification and rollback.",
"body_markdown": "# Runbook\n\n## Prerequisites\n\n## Procedure\n\n## Verification\n\n## Rollback\n",
"version": 1,
"is_system": true,
"created_at": "2026-08-16T00:00:00Z",
"updated_at": "2026-08-16T00:00:00Z"
}
]
}GET/api/projects/{projectId}/wiki/pages/{pageId}/export.mdExport page MarkdownUser token or API key
Streams the current canonical Markdown bytes without rendering or normalization. Archived pages can be exported. The download filename is sanitized from the page filename and capped at 180 bytes.
# GET /api/projects/{projectId}/wiki/pages/{pageId}/export.mdPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
ETag | strong ETag | Required | Current page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value. |
Content-Disposition | string | Required | attachment with safe filename and RFC 5987 filename* parameters. |
Content-Length | integer | Required | Exact Markdown byte count. |
X-Content-Type-Options | nosniff | Required | Prevents browser MIME sniffing. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
Content types: application/json → text/markdown; charset=utf-8
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" --output incident-response.md http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/export.mdResponse example
# Incident response
Follow the project runbook.
GET/api/projects/{projectId}/wiki/pages/{pageId}/attachmentsList page attachmentsUser token or API key
Returns ready image metadata and canonical wiki-attachment Markdown references for the page. Private storage bucket/path fields are never exposed.
# GET /api/projects/{projectId}/wiki/pages/{pageId}/attachmentsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
attachments | WikiAttachment[] | Required | Ready attachment metadata in storage query order. |
attachments[].id | UUID | Required | Attachment UUID. |
attachments[].project_id | UUID | Required | Owning project UUID. |
attachments[].page_id | UUID | Required | Owning wiki page UUID. |
attachments[].original_filename | string | Required | Sanitized image filename, at most 180 bytes. |
attachments[].content_type | image/png | image/jpeg | image/webp | Required | Verified media type detected from the bytes. |
attachments[].size_bytes | integer | Required | Image size from 1 byte through 8 MiB. |
attachments[].sha256 | string | Required | Lowercase SHA-256 hex digest. |
attachments[].width | integer | Required | Verified width from 1 through 8000 pixels. |
attachments[].height | integer | Required | Verified height from 1 through 8000 pixels. |
attachments[].actor_kind | user | api_key | system | Required | Uploading principal kind. |
attachments[].actor_user_id | UUID | null | Required | Uploading user ID, when applicable. |
attachments[].actor_api_key_id | UUID | null | Required | Uploading API key ID, when applicable. |
attachments[].actor_display | string | null | Required | Stored display identity for the uploader. |
attachments[].ready_at | ISO 8601 string | Required | Timestamp when storage and metadata became ready. |
attachments[].created_at | ISO 8601 string | Required | Creation timestamp. |
attachments[].markdown | string | Required | Canonical  reference. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/attachmentsResponse example
{
"attachments": [
{
"id": "d2600000-0000-0000-0000-000000000401",
"project_id": "b0000000-0000-0000-0000-000000000001",
"page_id": "d2600000-0000-0000-0000-000000000201",
"original_filename": "architecture.png",
"content_type": "image/png",
"size_bytes": 42137,
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"width": 1280,
"height": 720,
"actor_kind": "user",
"actor_user_id": "d2600000-0000-0000-0000-000000000301",
"actor_api_key_id": null,
"actor_display": "d2600000-0000-0000-0000-000000000301",
"ready_at": "2026-08-16T11:30:00Z",
"created_at": "2026-08-16T11:30:00Z",
"markdown": ""
}
]
}POST/api/projects/{projectId}/wiki/pages/{pageId}/attachmentsUpload a page imageUser token or API key
Accepts exactly one multipart file field named file on an active page. The request is capped at 10 MiB and the file at 8 MiB. Only fully decodable PNG, JPEG, and WebP bytes are accepted; declared media type and filename extension must match, dimensions must be 1-8000 pixels and at most 16 megapixels total, and the sanitized filename is capped at 180 bytes. Each project is capped at 200 images and 50 MiB.
# POST /api/projects/{projectId}/wiki/pages/{pageId}/attachmentsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
file | PNG | JPEG | WebP binary | Required | The only multipart part. It must have a filename and an image/png, image/jpeg, or image/webp Content-Type matching its bytes and extension. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
attachment.id | UUID | Required | Attachment UUID. |
attachment.project_id | UUID | Required | Owning project UUID. |
attachment.page_id | UUID | Required | Owning wiki page UUID. |
attachment.original_filename | string | Required | Sanitized image filename, at most 180 bytes. |
attachment.content_type | image/png | image/jpeg | image/webp | Required | Verified media type detected from the bytes. |
attachment.size_bytes | integer | Required | Image size from 1 byte through 8 MiB. |
attachment.sha256 | string | Required | Lowercase SHA-256 hex digest. |
attachment.width | integer | Required | Verified width from 1 through 8000 pixels. |
attachment.height | integer | Required | Verified height from 1 through 8000 pixels. |
attachment.actor_kind | user | api_key | system | Required | Uploading principal kind. |
attachment.actor_user_id | UUID | null | Required | Uploading user ID, when applicable. |
attachment.actor_api_key_id | UUID | null | Required | Uploading API key ID, when applicable. |
attachment.actor_display | string | null | Required | Stored display identity for the uploader. |
attachment.ready_at | ISO 8601 string | Required | Timestamp when storage and metadata became ready. |
attachment.created_at | ISO 8601 string | Required | Creation timestamp. |
attachment.markdown | string | Required | Canonical  reference. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
invalid_wiki_attachment_multipart | 400/415 | Optional | The request is not multipart/form-data or does not contain exactly one file part. |
invalid_wiki_attachment | 400 | Optional | The image part is empty. |
invalid_wiki_attachment_type | 415 | Optional | The file part has no valid declared Content-Type. |
unsupported_wiki_attachment_type | 415 | Optional | Detected bytes are not PNG, JPEG, or WebP. |
wiki_attachment_type_mismatch | 415 | Optional | Declared image media type does not match detected bytes. |
wiki_attachment_filename_type_mismatch | 400 | Optional | Filename extension does not match the image type. |
invalid_wiki_attachment_image | 400 | Optional | Image bytes are malformed or truncated. |
wiki_attachment_dimensions_exceeded | 400 | Optional | Image width or height is outside 1-8000 pixels, or total pixels exceed 16 megapixels. |
wiki_attachment_request_too_large | 413 | Optional | The multipart request exceeds 10 MiB. |
wiki_attachment_too_large | 413 | Optional | The image exceeds 8 MiB. |
wiki_page_archived | 409 | Optional | Archived pages cannot receive attachments. |
wiki_attachment_project_quota_exceeded | 409 | Optional | The project already has 200 images or 50 MiB of wiki image metadata. |
wiki_attachment_storage_unavailable | 503 | Optional | Private object storage could not accept the image. |
wiki_attachment_metadata_reconciliation_pending | 503 | Optional | The generated attachment ID could not be conclusively re-queried after an ambiguous insert response. Storage is preserved and a delayed reconciliation row prevents cleanup if metadata later appears. |
Content types: multipart/form-data; boundary=... → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" -F "[email protected];type=image/png" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/attachmentsResponse example
{
"attachment": {
"id": "d2600000-0000-0000-0000-000000000401",
"project_id": "b0000000-0000-0000-0000-000000000001",
"page_id": "d2600000-0000-0000-0000-000000000201",
"original_filename": "architecture.png",
"content_type": "image/png",
"size_bytes": 42137,
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"width": 1280,
"height": 720,
"actor_kind": "api_key",
"actor_user_id": null,
"actor_api_key_id": "d2600000-0000-0000-0000-000000000402",
"actor_display": "d2600000-0000-0000-0000-000000000402",
"ready_at": "2026-08-16T11:30:00Z",
"created_at": "2026-08-16T11:30:00Z",
"markdown": ""
}
}GET/api/projects/{projectId}/wiki/pages/{pageId}/attachments/{attachmentId}Create an attachment access URLUser token or API key
The sole browser/agent access broker for the private project-wiki-images bucket. It verifies project, page, attachment, bucket, and object-path metadata before returning a client-facing signed download URL valid for 300 seconds. The endpoint returns JSON, not the image bytes.
# GET /api/projects/{projectId}/wiki/pages/{pageId}/attachments/{attachmentId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
attachmentId | UUID | Required | Canonical UUID of an attachment on the page. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
attachment.id | UUID | Required | Attachment UUID. |
attachment.project_id | UUID | Required | Owning project UUID. |
attachment.page_id | UUID | Required | Owning wiki page UUID. |
attachment.original_filename | string | Required | Sanitized image filename, at most 180 bytes. |
attachment.content_type | image/png | image/jpeg | image/webp | Required | Verified media type detected from the bytes. |
attachment.size_bytes | integer | Required | Image size from 1 byte through 8 MiB. |
attachment.sha256 | string | Required | Lowercase SHA-256 hex digest. |
attachment.width | integer | Required | Verified width from 1 through 8000 pixels. |
attachment.height | integer | Required | Verified height from 1 through 8000 pixels. |
attachment.actor_kind | user | api_key | system | Required | Uploading principal kind. |
attachment.actor_user_id | UUID | null | Required | Uploading user ID, when applicable. |
attachment.actor_api_key_id | UUID | null | Required | Uploading API key ID, when applicable. |
attachment.actor_display | string | null | Required | Stored display identity for the uploader. |
attachment.ready_at | ISO 8601 string | Required | Timestamp when storage and metadata became ready. |
attachment.created_at | ISO 8601 string | Required | Creation timestamp. |
attachment.markdown | string | Required | Canonical  reference. |
download_url | URL | Required | Short-lived signed private-storage URL. |
expires_at | ISO 8601 string | Required | Approximate UTC expiry timestamp. |
expires_in_seconds | integer | Required | Signed URL lifetime; always 300. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
wiki_attachment_not_found | 404 | Optional | The attachment ID is invalid or does not belong to this page. |
wiki_attachment_unavailable | 503 | Optional | The attachment operation is temporarily unavailable. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/attachments/d2600000-0000-0000-0000-000000000401Response example
{
"attachment": {
"id": "d2600000-0000-0000-0000-000000000401",
"project_id": "b0000000-0000-0000-0000-000000000001",
"page_id": "d2600000-0000-0000-0000-000000000201",
"original_filename": "architecture.png",
"content_type": "image/png",
"size_bytes": 42137,
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"width": 1280,
"height": 720,
"actor_kind": "user",
"actor_user_id": "d2600000-0000-0000-0000-000000000301",
"actor_api_key_id": null,
"actor_display": "d2600000-0000-0000-0000-000000000301",
"ready_at": "2026-08-16T11:30:00Z",
"created_at": "2026-08-16T11:30:00Z",
"markdown": ""
},
"download_url": "http://localhost:54421/storage/v1/object/sign/project-wiki-images/...",
"expires_at": "2026-08-16T11:35:00Z",
"expires_in_seconds": 300
}DELETE/api/projects/{projectId}/wiki/pages/{pageId}/attachments/{attachmentId}Delete a wiki attachmentUser token
JWT-admin-only destructive removal. API keys are rejected regardless of scope. A database trigger serializes the operation and returns wiki_attachment_in_use while current or immutable historical Markdown references the attachment. Otherwise metadata is deleted first and the private object path is durably queued in that transaction; storage is then removed and the queue row finalized.
# DELETE /api/projects/{projectId}/wiki/pages/{pageId}/attachments/{attachmentId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
pageId | UUID | Required | Canonical UUID of a wiki page in the project. |
attachmentId | UUID | Required | Canonical UUID of an attachment on the page. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
deleted | boolean | Required | True after object and metadata deletion. |
attachment.id | UUID | Required | Attachment UUID. |
attachment.project_id | UUID | Required | Owning project UUID. |
attachment.page_id | UUID | Required | Owning wiki page UUID. |
attachment.original_filename | string | Required | Sanitized image filename, at most 180 bytes. |
attachment.content_type | image/png | image/jpeg | image/webp | Required | Verified media type detected from the bytes. |
attachment.size_bytes | integer | Required | Image size from 1 byte through 8 MiB. |
attachment.sha256 | string | Required | Lowercase SHA-256 hex digest. |
attachment.width | integer | Required | Verified width from 1 through 8000 pixels. |
attachment.height | integer | Required | Verified height from 1 through 8000 pixels. |
attachment.actor_kind | user | api_key | system | Required | Uploading principal kind. |
attachment.actor_user_id | UUID | null | Required | Uploading user ID, when applicable. |
attachment.actor_api_key_id | UUID | null | Required | Uploading API key ID, when applicable. |
attachment.actor_display | string | null | Required | Stored display identity for the uploader. |
attachment.ready_at | ISO 8601 string | Required | Timestamp when storage and metadata became ready. |
attachment.created_at | ISO 8601 string | Required | Creation timestamp. |
attachment.markdown | string | Required | Canonical  reference. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_admin_required | 403 | Optional | The operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it. |
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_page_not_found | 404 | Optional | The page ID is invalid or no matching page exists in this project. |
wiki_attachment_not_found | 404 | Optional | The attachment ID is invalid or does not belong to this page. |
wiki_attachment_unavailable | 503 | Optional | The attachment operation is temporarily unavailable. |
wiki_attachment_in_use | 409 | Optional | Current or immutable historical page content still references the attachment. |
wiki_attachment_delete_metadata_failed | 503 | Optional | The database could not delete attachment metadata; storage is untouched. |
wiki_attachment_delete_reconciliation_pending | 503 | Optional | The metadata-delete response was ambiguous. Storage is untouched and any committed deletion remains durably queued. |
wiki_attachment_delete_cleanup_pending | 503 | Optional | Metadata was deleted but storage removal or queue finalization failed. The durable queue retries idempotent object removal; details reports completed phases. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/pages/d2600000-0000-0000-0000-000000000201/attachments/d2600000-0000-0000-0000-000000000401Response example
{
"deleted": true,
"attachment": {
"id": "d2600000-0000-0000-0000-000000000401",
"project_id": "b0000000-0000-0000-0000-000000000001",
"page_id": "d2600000-0000-0000-0000-000000000201",
"original_filename": "architecture.png",
"content_type": "image/png",
"size_bytes": 42137,
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"width": 1280,
"height": 720,
"actor_kind": "user",
"actor_user_id": "d2600000-0000-0000-0000-000000000301",
"actor_api_key_id": null,
"actor_display": "d2600000-0000-0000-0000-000000000301",
"ready_at": "2026-08-16T11:30:00Z",
"created_at": "2026-08-16T11:30:00Z",
"markdown": ""
}
}GET/api/projects/{projectId}/wiki/export.zipExport the project wiki ZIPUser token or API key
Builds and validates a deterministic current-state ZIP before streaming it. It contains active page heads only, ready image attachments, and root wiki-manifest.json version 1. Limits are 200 pages, 200 attachments, 500 entries, 50 MiB expanded, and 25 MiB output. Archived pages and their attachments are excluded; export rejects active pages whose stable links target an excluded page.
# GET /api/projects/{projectId}/wiki/export.zipPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
Response headers
| Field | Type / status | Required | Description |
|---|---|---|---|
Content-Disposition | string | Required | attachment filename project-{projectId}-wiki.zip. |
Content-Length | integer | Required | Exact completed ZIP byte count. |
X-Content-Type-Options | nosniff | Required | Prevents browser MIME sniffing. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_archive_expanded_too_large | 413 | Optional | An entry or the complete ZIP exceeds 50 MiB expanded. |
wiki_archive_page_limit | 413 | Optional | The archive or project contains more than 200 pages. |
wiki_archive_attachment_limit | 413 | Optional | The archive or project contains more than 200 attachments. |
wiki_archive_output_too_large | 413 | Optional | The generated ZIP exceeds the 25 MiB round-trip limit. |
wiki_archive_attachment_corrupt | 409 | Optional | Stored attachment bytes do not match ready metadata. |
wiki_archive_dangling_page_reference | 409 | Optional | An active page contains a stable link to a page excluded from the archive. |
wiki_archive_storage_unavailable | 503 | Optional | An attachment could not be downloaded for export. |
Content types: application/json → application/zip
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" --output project-wiki.zip http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/export.zipResponse example
Binary ZIP bytes beginning with PK (0x50 0x4b).POST/api/projects/{projectId}/wiki/importImport a project wiki ZIPUser token
JWT-admin-only import of the version-1 current-state archive contract. API keys are rejected regardless of scope. The multipart request must contain exactly one application/zip file field named file. Limits are 25 MiB compressed plus 1 MiB multipart overhead, 50 MiB expanded, 500 entries, 200 pages, 200 attachments, 100:1 compression, 1 MiB per Markdown page, and 8 MiB per image. Import creates new destination IDs and revision 1 pages; image references are rewritten. Exact Idempotency-Key retries are replayed by archive SHA-256.
# POST /api/projects/{projectId}/wiki/importPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | UUID | Required | Canonical UUID of the project that owns the independent wiki. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Optional | Required for API-key commands and optional for JWT commands. After trimming, must be 1-255 Unicode characters. An exact retry by the same actor replays the saved result; reuse for different input returns idempotency_mismatch. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
file | application/zip binary | Required | The only multipart part. The ZIP must contain root wiki-manifest.json version 1, pages/{filename} entries, and attachments/{sourceAttachmentId}/{filename} entries exactly referenced by the manifest. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
outcome | applied | Required | Successful import outcome. |
code | wiki_archive_imported | Required | Stable success code. |
project_id | UUID | Required | Destination project UUID. |
archive_sha256 | string | Required | SHA-256 of the uploaded ZIP bytes. |
page_count | integer | Required | Number of created pages. |
attachment_count | integer | Required | Number of created attachments. |
pages | WikiArchiveImportPage[] | Required | Mappings with source_id, destination id, title, slug, filename, source_revision, revision (1), and audit_event_id. |
attachments | WikiArchiveImportAttachment[] | Required | Mappings with source_id, destination id, destination page_id, and private object path. |
replayed | boolean | Required | True for an exact saved retry. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
project_not_found | 404 | Optional | The project ID is invalid or the project does not exist. |
project_access_denied | 403 | Optional | The JWT user cannot access the project or the API key is bound to another project. |
authentication_required | 401 | Optional | The authenticated JWT user could not be identified. |
wiki_admin_required | 403 | Optional | The operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it. |
invalid_idempotency_key | 400 | Optional | An API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters. |
idempotency_mismatch | 409 | Optional | The same actor already used this Idempotency-Key for different canonical command input. |
invalid_wiki_archive | 400 | Optional | The ZIP is malformed, truncated, or fails CRC/length validation. |
invalid_wiki_archive_manifest | 400 | Optional | wiki-manifest.json is not the exact valid version-1 schema. |
wiki_archive_manifest_missing | 400 | Optional | wiki-manifest.json is absent from the ZIP root. |
unsupported_wiki_archive_version | 400 | Optional | The manifest version is not 1. |
unsafe_wiki_archive_path | 400 | Optional | An entry path is absolute, non-canonical, non-ASCII, or traverses directories. |
duplicate_wiki_archive_path | 400 | Optional | Two ZIP paths normalize to the same case-insensitive path. |
unsupported_wiki_archive_entry | 400 | Optional | An entry is encrypted, not a regular file, or uses unsupported compression. |
nested_wiki_archive | 400 | Optional | The ZIP contains another archive file. |
invalid_wiki_archive_page | 400 | Optional | A page manifest entry or its canonical Markdown metadata is invalid. |
wiki_archive_page_missing | 400 | Optional | A manifest page path is absent from the ZIP. |
invalid_wiki_archive_attachment | 400 | Optional | An attachment manifest entry, filename, media type, or metadata is invalid. |
wiki_archive_attachment_missing | 400 | Optional | A manifest attachment path is absent from the ZIP. |
wiki_archive_attachment_size_mismatch | 400 | Optional | Attachment bytes do not match the manifest size. |
wiki_archive_attachment_hash_mismatch | 400 | Optional | Attachment bytes do not match the manifest SHA-256. |
invalid_wiki_archive_markdown | 400 | Optional | A Markdown entry is not valid UTF-8 or contains NUL. |
wiki_archive_page_collision | 409 | Optional | Imported page IDs, slugs, filenames, or paths collide. |
wiki_archive_id_collision | 409 | Optional | Manifest page and attachment IDs collide. |
wiki_archive_attachment_collision | 409 | Optional | Manifest attachment paths collide. |
wiki_archive_compressed_too_large | 413 | Optional | The ZIP exceeds 25 MiB compressed. |
wiki_archive_expanded_too_large | 413 | Optional | An entry or the complete ZIP exceeds 50 MiB expanded. |
wiki_archive_compression_ratio | 413 | Optional | An entry or the complete ZIP exceeds a 100:1 expansion ratio. |
wiki_archive_entry_limit | 413 | Optional | The ZIP does not contain 1-500 regular file entries. |
wiki_archive_page_limit | 413 | Optional | The archive or project contains more than 200 pages. |
wiki_archive_attachment_limit | 413 | Optional | The archive or project contains more than 200 attachments. |
wiki_archive_markdown_too_large | 413 | Optional | One imported Markdown page exceeds 1 MiB. |
wiki_archive_output_too_large | 413 | Optional | The generated ZIP exceeds the 25 MiB round-trip limit. |
invalid_wiki_archive_multipart | 400/415 | Optional | The request is not the exact one-file application/zip multipart contract. |
unexpected_wiki_archive_entry | 400 | Optional | The ZIP has a file not referenced by the manifest. |
wiki_attachment_project_quota_exceeded | 409 | Optional | Import would exceed the destination project's 200-image or 50 MiB wiki quota. |
wiki_archive_storage_unavailable | 503 | Optional | Attachment staging failed and completed cleanup succeeded. |
wiki_archive_staging_cleanup_pending | 503 | Optional | Attachment staging failed and storage cleanup remains pending. |
wiki_archive_database_cleanup_pending | 503 | Optional | Publication failed and staged-object cleanup remains pending. |
wiki_archive_replay_cleanup_pending | 503 | Optional | Replay succeeded but newly staged unused objects could not be removed. |
Content types: multipart/form-data; boundary=... → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" -H "Idempotency-Key: import-project-wiki-2026-08-16" -F "[email protected];type=application/zip" http://localhost:8080/api/projects/b0000000-0000-0000-0000-000000000001/wiki/importResponse example
{
"outcome": "applied",
"code": "wiki_archive_imported",
"project_id": "b0000000-0000-0000-0000-000000000001",
"archive_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"page_count": 1,
"attachment_count": 1,
"pages": [
{
"source_id": "d2600000-0000-0000-0000-000000000201",
"id": "d2600000-0000-0000-0000-000000000801",
"title": "Incident response",
"slug": "incident-response",
"filename": "runbooks/incident-response.md",
"source_revision": 3,
"revision": 1,
"audit_event_id": "d2600000-0000-0000-0000-000000000802"
}
],
"attachments": [
{
"source_id": "d2600000-0000-0000-0000-000000000401",
"id": "d2600000-0000-0000-0000-000000000803",
"page_id": "d2600000-0000-0000-0000-000000000801",
"path": "b0000000-0000-0000-0000-000000000001/d2600000-0000-0000-0000-000000000801/d2600000-0000-0000-0000-000000000803/architecture.png"
}
],
"replayed": false
}Agent App Control
Durable human approval for high-impact project actions requested by external coding agents.
POST/api/projects/{projectId}/app-control/requestsRequest approval for a high-impact actionUser token or API key
A project API key submits an exact typed action. BernOps stores only a request hash and safe resource summary; no domain mutation occurs.
# POST /api/projects/{projectId}/app-control/requestsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | API key's project UUID. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Required | Stable logical-request identity. Reuse only for an exact retry. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
operation | string | Required | Allowlisted high-impact app-control operation. |
call | object | Required | Exact typed params, query, body, and mutation preconditions for the operation. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
request_id | string | Required | Durable approval request UUID. |
project_id | string | Required | Project derived from the API key or stored request. |
operation | string | Required | High-impact operation awaiting or using approval. |
safe_summary | object | Required | Server-derived operation and resource IDs; never contains the request body. |
status | string | Required | pending, approved, denied, executing, completed, failed, or expired. |
expires_at | string | Required | Approval expiry timestamp. |
approval_path | string | Required | Static browser route where a project administrator reviews the action. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
app_control_approval_not_required | 400 | Optional | The operation is routine and must use its normal endpoint. |
app_control_insufficient_scope | 403 | Optional | The key lacks the operation's existing scope. |
app_control_idempotency_mismatch | 409 | Optional | The idempotency key was reused for a different action. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer $BERNOPS_API_KEY" \
-H "Idempotency-Key: approval-delete-label-v1" -H "Content-Type: application/json" \
-d '{"operation":"label.delete","call":{"params":{"labelId":"LABEL_UUID"},"mutation":{"ifMatch":"\"7\""}}}' \
http://localhost:8080/api/projects/PROJECT_UUID/app-control/requestsResponse example
{
"request_id": "request-uuid",
"project_id": "project-uuid",
"operation": "label.delete",
"safe_summary": {
"operation": "label.delete",
"resource_ids": {
"labelId": "label-uuid"
}
},
"status": "pending",
"expires_at": "2026-08-28T18:00:00Z",
"approval_path": "/projects/agents/approvals?projectId=project-uuid&requestId=request-uuid"
}GET/api/app-control/requests/{requestId}Get approval statusUser token or API key
Returns safe approval metadata to the originating API key or a project-visible JWT user.
# GET /api/app-control/requests/{requestId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
requestId | string | Required | Approval request UUID. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
request_id | string | Required | Durable approval request UUID. |
project_id | string | Required | Project derived from the API key or stored request. |
operation | string | Required | High-impact operation awaiting or using approval. |
safe_summary | object | Required | Server-derived operation and resource IDs; never contains the request body. |
status | string | Required | pending, approved, denied, executing, completed, failed, or expired. |
expires_at | string | Required | Approval expiry timestamp. |
approval_path | string | Required | Static browser route where a project administrator reviews the action. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer $BERNOPS_API_KEY" http://localhost:8080/api/app-control/requests/REQUEST_UUIDResponse example
{
"request_id": "request-uuid",
"status": "approved"
}POST/api/app-control/requests/{requestId}/decisionApprove or deny an actionUser token
A current project administrator makes the independent human decision. API keys cannot call this endpoint.
# POST /api/app-control/requests/{requestId}/decisionPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
requestId | string | Required | Approval request UUID. |
Request headers
| Field | Type / status | Required | Description |
|---|---|---|---|
Idempotency-Key | string | Required | Stable logical-request identity. Reuse only for an exact retry. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
approved | boolean | Required | True to approve, false to deny. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
request_id | string | Required | Durable approval request UUID. |
project_id | string | Required | Project derived from the API key or stored request. |
operation | string | Required | High-impact operation awaiting or using approval. |
safe_summary | object | Required | Server-derived operation and resource IDs; never contains the request body. |
status | string | Required | pending, approved, denied, executing, completed, failed, or expired. |
expires_at | string | Required | Approval expiry timestamp. |
approval_path | string | Required | Static browser route where a project administrator reviews the action. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
app_control_admin_required | 403 | Optional | The user is not a project administrator. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer $JWT" -H "Idempotency-Key: decision-v1" \
-H "Content-Type: application/json" -d '{"approved":true}' \
http://localhost:8080/api/app-control/requests/REQUEST_UUID/decisionResponse example
{
"request_id": "request-uuid",
"status": "approved"
}POST/api/app-control/requests/{requestId}/executeExecute an approved actionUser token or API key
The same current API key resubmits the exact semantic action. BernOps rechecks scopes, project, approval, request hash, and domain preconditions before one replay-safe execution.
# POST /api/app-control/requests/{requestId}/executePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
requestId | string | Required | Approved request UUID. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
operation | string | Required | Allowlisted high-impact app-control operation. |
call | object | Required | Exact typed params, query, body, and mutation preconditions for the operation. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
result | object | Required | Canonical result from the underlying domain command. |
Error codes
| Field | Type / status | Required | Description |
|---|---|---|---|
app_control_approval_pending | 409 | Optional | The request still awaits a human decision. |
app_control_request_denied | 403 | Optional | The request was denied. |
app_control_request_expired | 410 | Optional | The request expired. |
app_control_request_not_found | 404 | Optional | Key, project, operation, or request hash did not match. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer $BERNOPS_API_KEY" -H "Content-Type: application/json" \
-d '{"operation":"label.delete","call":{"params":{"labelId":"LABEL_UUID"},"mutation":{"ifMatch":"\"7\""}}}' \
http://localhost:8080/api/app-control/requests/REQUEST_UUID/executeResponse example
{
"outcome": "applied",
"code": "label_deleted"
}Triggers
Manage cron-based triggers that automatically execute jobs on a schedule.
GET/api/projects/{projectId}/triggersList triggersUser token or API key
Returns all triggers configured for the specified project.
# GET /api/projects/{projectId}/triggersPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Trigger UUID. |
name | string | Required | Trigger display name. |
job_id | string | Required | UUID of the associated job. |
cron_expression | string | Required | Cron schedule expression. |
enabled | boolean | Required | Whether the trigger is active. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/triggersResponse example
[
{
"id": "trig-001",
"name": "Nightly Login Test",
"job_id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"cron_expression": "0 2 * * *",
"enabled": true
}
]POST/api/projects/{projectId}/triggersCreate triggerUser token or API key
Creates a new cron-based trigger that will execute the specified job on schedule. Project API keys need triggers:write.
# POST /api/projects/{projectId}/triggersPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Trigger display name. |
job_id | string | Required | UUID of the job to execute. |
cron_expression | string | Required | Cron schedule expression (5-field). |
enabled | boolean | Optional | Whether the trigger is active. Defaults to true. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | UUID of the newly created trigger. |
name | string | Required | Trigger name. |
cron_expression | string | Required | Cron schedule. |
enabled | boolean | Required | Active state. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Hourly Smoke Test","job_id":"e5f6a7b8-...","cron_expression":"0 * * * *"}' \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/triggersResponse example
{
"id": "trig-002",
"name": "Hourly Smoke Test",
"cron_expression": "0 * * * *",
"enabled": true,
"created_at": "2026-04-04T14:00:00Z"
}GET/api/triggers/{triggerId}Get triggerUser token or API key
Returns the full details of a single trigger.
# GET /api/triggers/{triggerId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
triggerId | string | Required | UUID of the trigger. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Trigger UUID. |
name | string | Required | Trigger name. |
job_id | string | Required | Associated job UUID. |
cron_expression | string | Required | Cron schedule expression. |
enabled | boolean | Required | Whether the trigger is active. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/triggers/trig-001Response example
{
"id": "trig-001",
"name": "Nightly Login Test",
"job_id": "e5f6a7b8-c9d0-1234-efab-345678901234",
"cron_expression": "0 2 * * *",
"enabled": true,
"created_at": "2026-03-15T09:00:00Z"
}PUT/api/triggers/{triggerId}Update triggerUser token or API key
Updates an existing trigger. Project API keys need triggers:write. Only provided fields are changed.
# PUT /api/triggers/{triggerId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
triggerId | string | Required | UUID of the trigger. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | New trigger name. |
job_id | string | Optional | New job UUID. |
cron_expression | string | Optional | New cron schedule expression. |
enabled | boolean | Optional | Enable or disable the trigger. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Trigger UUID. |
name | string | Required | Updated trigger name. |
cron_expression | string | Required | Updated cron schedule. |
enabled | boolean | Required | Updated active state. |
updated_at | string | Required | ISO 8601 update timestamp. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"cron_expression":"30 1 * * *"}' \
http://localhost:8080/api/triggers/trig-001Response example
{
"id": "trig-001",
"name": "Nightly Login Test v2",
"cron_expression": "30 1 * * *",
"enabled": true,
"updated_at": "2026-04-04T14:30:00Z"
}DELETE/api/triggers/{triggerId}Delete triggerUser token or API key
Permanently deletes a trigger. Project API keys need triggers:write. The associated job is not affected.
# DELETE /api/triggers/{triggerId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
triggerId | string | Required | UUID of the trigger to delete. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
message | string | Required | Confirmation message. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/triggers/trig-001Response example
{
"message": "Trigger deleted successfully."
}Queues
Manage work queues for RPA automation. Queues hold items that can be processed by workers sequentially or in parallel.
GET/api/projects/{projectId}/queuesList queuesUser token or API key
Returns all queues configured for the specified project.
# GET /api/projects/{projectId}/queuesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue UUID. |
name | string | Required | Queue display name. |
description | string | Optional | Optional queue description. |
max_retries | number | Required | Maximum retry attempts per item. |
custom_statuses | string[] | Required | Queue-specific custom statuses. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/queuesResponse example
[
{
"id": "queue-001",
"name": "Invoice Processing",
"description": "Incoming invoices to be processed.",
"max_retries": 3,
"custom_statuses": [
"blocked"
],
"created_at": "2026-03-20T08:00:00Z"
}
]POST/api/projects/{projectId}/queuesCreate queueUser token or API key
Creates a new work queue within the specified project.
# POST /api/projects/{projectId}/queuesPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Queue display name. |
description | string | Optional | Optional description. |
max_retries | number | Optional | Maximum retry attempts per item. Defaults to 3. |
retry_delay_seconds | number | Optional | Delay between retries in seconds. Defaults to 60. |
deadline_seconds | number | Optional | Maximum time in seconds before an in-progress item is considered timed out. |
auto_trigger_job_id | string | Optional | UUID of a job to auto-trigger when items are added. |
custom_statuses | string[] | Optional | Additional queue-specific statuses allowed by the custom status endpoint. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | UUID of the newly created queue. |
name | string | Required | Queue name. |
max_retries | number | Required | Configured max retries. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Order Fulfillment","max_retries":5,"retry_delay_seconds":120,"custom_statuses":["blocked"]}' \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/queuesResponse example
{
"id": "queue-002",
"name": "Order Fulfillment",
"max_retries": 5,
"custom_statuses": [
"blocked"
],
"created_at": "2026-04-04T15:00:00Z"
}GET/api/queues/{queueId}Get queueUser token or API key
Returns the full details of a single queue, including its configuration.
# GET /api/queues/{queueId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue UUID. |
name | string | Required | Queue name. |
description | string | Optional | Queue description. |
max_retries | number | Required | Maximum retries. |
retry_delay_seconds | number | Required | Delay between retries. |
deadline_seconds | number | Optional | Item processing deadline. |
custom_statuses | string[] | Required | Queue-specific custom statuses. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queues/queue-001Response example
{
"id": "queue-001",
"name": "Invoice Processing",
"description": "Incoming invoices to be processed.",
"max_retries": 3,
"retry_delay_seconds": 60,
"deadline_seconds": 300,
"custom_statuses": [
"blocked"
],
"created_at": "2026-03-20T08:00:00Z"
}PUT/api/queues/{queueId}Update queueUser token or API key
Updates an existing queue configuration. Only provided fields are changed.
# PUT /api/queues/{queueId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Optional | New queue name. |
description | string | Optional | New description. |
max_retries | number | Optional | New max retry count. |
retry_delay_seconds | number | Optional | New retry delay. |
deadline_seconds | number | Optional | New processing deadline. |
auto_trigger_job_id | string | Optional | New auto-trigger job UUID. |
custom_statuses | string[] | Optional | Replace the queue-specific custom statuses. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue UUID. |
name | string | Required | Updated queue name. |
max_retries | number | Required | Updated max retries. |
updated_at | string | Required | ISO 8601 update timestamp. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"max_retries":5,"custom_statuses":["blocked"]}' \
http://localhost:8080/api/queues/queue-001Response example
{
"id": "queue-001",
"name": "Invoice Processing v2",
"max_retries": 5,
"custom_statuses": [
"blocked"
],
"updated_at": "2026-04-04T15:30:00Z"
}DELETE/api/queues/{queueId}Delete queueUser token or API key
Permanently deletes a queue and all its items. This action cannot be undone.
# DELETE /api/queues/{queueId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue to delete. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
message | string | Required | Confirmation message. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queues/queue-001Response example
{
"message": "Queue deleted successfully."
}GET/api/queues/{queueId}/statsGet queue statsUser token or API key
Returns aggregated statistics for a queue: item counts by status, throughput, and error rates.
# GET /api/queues/{queueId}/statsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
total | number | Required | Total items in the queue. |
status_counts | object | Required | Counts keyed by item status. |
avg_processing_ms | number | Required | Average processing duration for completed items. |
due_today | number | Required | New items due by the end of today. |
retry_rate | number | Required | Percentage of items that have retried at least once. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queues/queue-001/statsResponse example
{
"total": 250,
"status_counts": {
"new": 45,
"in_progress": 3,
"completed": 190,
"failed": 12
},
"avg_processing_ms": 1830,
"due_today": 8,
"retry_rate": 4.8
}Queue Items
Manage individual items within a work queue. Items flow through a fixed lifecycle: new, in_progress, completed, failed, or abandoned.
GET/api/queues/{queueId}/itemsList queue itemsUser token or API key
Returns items in the specified queue, with optional filtering by status and reference.
# GET /api/queues/{queueId}/itemsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Query parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by status: "new", "in_progress", "completed", "failed", "abandoned". |
reference | string | Optional | Filter by reference identifier. |
limit | number | Optional | Maximum number of items to return. (Default: 50) |
offset | number | Optional | Number of items to skip for pagination. (Default: 0) |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Item status. |
reference | string | Optional | External reference identifier. |
priority | number | Required | Item priority (higher = more urgent). |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
"http://localhost:8080/api/queues/queue-001/items?status=new&limit=20"Response example
[
{
"id": "item-001",
"status": "new",
"reference": "INV-2026-0042",
"priority": 10,
"created_at": "2026-04-04T10:00:00Z"
}
]POST/api/queues/{queueId}/itemsAdd queue itemUser token or API key
Adds a single item to the queue for processing.
# POST /api/queues/{queueId}/itemsPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | object | Required | Arbitrary JSON payload for the item. |
reference | string | Optional | External reference identifier (e.g. invoice number). |
priority | number | Optional | Item priority. Higher values are processed first. |
tags | string[] | Optional | Tags for categorization and filtering. |
due_date | string | Optional | ISO 8601 deadline for processing this item. |
defer_until | string | Optional | ISO 8601 timestamp before which the item should not be processed. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | UUID of the newly created item. |
status | string | Required | Initial status ("new"). |
reference | string | Optional | Reference identifier. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"data":{"invoice_id":"INV-2026-0043","amount":150.00},"reference":"INV-2026-0043","priority":5}' \
http://localhost:8080/api/queues/queue-001/itemsResponse example
{
"id": "item-002",
"status": "new",
"reference": "INV-2026-0043",
"created_at": "2026-04-04T15:00:00Z"
}POST/api/queues/{queueId}/items/bulkBulk add queue itemsUser token or API key
Adds multiple items to the queue in a single request. Each item follows the same schema as the single-add endpoint.
# POST /api/queues/{queueId}/items/bulkPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
items | QueueItemInput[] | Required | Array of items to add. Each item has: data (required), reference, priority, tags, due_date, defer_until. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
created | number | Required | Number of items successfully created. |
items | QueueItem[] | Required | Array of created item summaries. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"items":[{"data":{"order":"ORD-001"},"reference":"ORD-001"},{"data":{"order":"ORD-002"},"reference":"ORD-002"}]}' \
http://localhost:8080/api/queues/queue-001/items/bulkResponse example
{
"created": 3,
"items": [
{
"id": "item-003",
"status": "new",
"reference": "ORD-001"
},
{
"id": "item-004",
"status": "new",
"reference": "ORD-002"
},
{
"id": "item-005",
"status": "new",
"reference": "ORD-003"
}
]
}POST/api/queues/{queueId}/nextGet next queue itemUser token or API key
Atomically fetches and locks the next available item from the queue. The item transitions to in_progress status.
# POST /api/queues/{queueId}/nextPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
queueId | string | Required | UUID of the queue. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Updated status ("in_progress"). |
data | object | Required | Item payload. |
reference | string | Optional | External reference. |
retry_count | number | Required | Number of times this item has been retried. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queues/queue-001/nextResponse example
{
"id": "item-001",
"status": "in_progress",
"data": {
"invoice_id": "INV-2026-0042",
"amount": 99.99
},
"reference": "INV-2026-0042",
"retry_count": 0
}GET/api/queue-items/{itemId}Get queue itemUser token or API key
Returns the full details of a single queue item, including its data payload and processing history.
# GET /api/queue-items/{itemId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
queue_id | string | Required | UUID of the parent queue. |
status | string | Required | Current item status. |
data | object | Required | Item payload. |
reference | string | Optional | External reference. |
retry_count | number | Required | Number of retries attempted. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queue-items/item-001Response example
{
"id": "item-001",
"queue_id": "queue-001",
"status": "in_progress",
"data": {
"invoice_id": "INV-2026-0042",
"amount": 99.99
},
"reference": "INV-2026-0042",
"retry_count": 0,
"created_at": "2026-04-04T10:00:00Z"
}PUT/api/queue-items/{itemId}Update queue itemUser token or API key
Updates an existing queue item's data, reference, priority, or tags.
# PUT /api/queue-items/{itemId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
data | object | Optional | Updated item payload. |
reference | string | Optional | Updated reference identifier. |
priority | number | Optional | Updated priority. |
tags | string[] | Optional | Updated tags. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Current item status. |
updated_at | string | Required | ISO 8601 update timestamp. |
Content types: application/json → application/json
cURL example
curl -X PUT -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"priority":20,"tags":["urgent"]}' \
http://localhost:8080/api/queue-items/item-001Response example
{
"id": "item-001",
"status": "new",
"updated_at": "2026-04-04T16:00:00Z"
}POST/api/queue-items/{itemId}/statusSet queue item statusUser token or API key
Sets a queue item to a default status or a custom status configured on the queue.
# POST /api/queue-items/{itemId}/statusPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
status | string | Required | Default or queue-defined custom status. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Updated item status. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"status":"failed"}' \
http://localhost:8080/api/queue-items/item-001/statusResponse example
{
"id": "item-001",
"status": "failed"
}DELETE/api/queue-items/{itemId}Delete queue itemUser token or API key
Permanently removes a queue item. Only allowed for items in new or terminal states.
# DELETE /api/queue-items/{itemId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item to delete. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
message | string | Required | Confirmation message. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queue-items/item-001Response example
{
"message": "Queue item deleted successfully."
}POST/api/queue-items/{itemId}/completeComplete queue itemUser token or API key
Marks an in-progress queue item as successfully completed. Optionally attach output data.
# POST /api/queue-items/{itemId}/completePath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
output | object | Optional | Optional output data from processing (results, generated files, etc.). |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Updated status ("completed"). |
completed_at | string | Required | ISO 8601 completion timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"output":{"processed":true,"pdf_url":"https://storage.example.com/inv-42.pdf"}}' \
http://localhost:8080/api/queue-items/item-001/completeResponse example
{
"id": "item-001",
"status": "completed",
"completed_at": "2026-04-04T16:05:00Z"
}POST/api/queue-items/{itemId}/failFail queue itemUser token or API key
Marks an in-progress queue item as failed. Optionally attach an error message. If retries remain, the item may be automatically requeued.
# POST /api/queue-items/{itemId}/failPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
error_message | string | Optional | Error message describing the failure. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Updated status ("failed"). |
retry_count | number | Required | Current retry count. |
max_retries | number | Required | Max retries configured for the queue. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{"error_message":"Timeout waiting for page load"}' \
http://localhost:8080/api/queue-items/item-001/failResponse example
{
"id": "item-001",
"status": "failed",
"retry_count": 2,
"max_retries": 3
}POST/api/queue-items/{itemId}/abandonAbandon queue itemUser token or API key
Marks an in-progress queue item as abandoned without counting it as a retry.
# POST /api/queue-items/{itemId}/abandonPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Updated status ("abandoned"). |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queue-items/item-001/abandonResponse example
{
"id": "item-001",
"status": "abandoned"
}POST/api/queue-items/{itemId}/retryRetry queue itemUser token or API key
Manually retries a failed queue item by resetting it to new status and incrementing the retry count.
# POST /api/queue-items/{itemId}/retryPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
itemId | string | Required | UUID of the queue item. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | Queue item UUID. |
status | string | Required | Updated status ("new"). |
retry_count | number | Required | Updated retry count. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer bo_live_your_api_key" \
http://localhost:8080/api/queue-items/item-001/retryResponse example
{
"id": "item-001",
"status": "new",
"retry_count": 3
}API Keys
Manage API keys for programmatic access. API keys provide an alternative to JWT tokens for automation and CI/CD integrations.
POST/api/projects/{projectId}/api-keysCreate API keyUser token
Creates a new project-scoped API key. The raw key value is returned only once in the response.
# POST /api/projects/{projectId}/api-keysPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project that will own the key. |
Request body
| Field | Type / status | Required | Description |
|---|---|---|---|
name | string | Required | Display name for the API key. |
scopes | string[] | Required | Array of granted scopes such as "jobs:read", "runs:write", or "queues:execute". |
expires_at | string | Optional | ISO 8601 expiration timestamp. Omit for a non-expiring key. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | API key UUID. |
key | string | Required | The raw API key value (bo_live_...). Shown only once. |
name | string | Required | Key display name. |
scopes | string[] | Required | Granted scopes. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -X POST -H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"CI Pipeline","scopes":["jobs:read","queues:execute"]}' \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/api-keysResponse example
{
"id": "key-001",
"key": "bo_live_abc123def456ghi789jkl012mno345",
"name": "CI Pipeline",
"scopes": [
"jobs:read",
"queues:execute"
],
"created_at": "2026-04-04T17:00:00Z"
}GET/api/projects/{projectId}/api-keysList API keysUser token
Returns all API keys for the specified project. The raw key value is never returned.
# GET /api/projects/{projectId}/api-keysPath parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
projectId | string | Required | UUID of the project. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
id | string | Required | API key UUID. |
name | string | Required | Key display name. |
key_prefix | string | Required | First 12 characters of the key for identification. |
scopes | string[] | Required | Granted scopes. |
last_used_at | string | Optional | ISO 8601 timestamp of last use. |
created_at | string | Required | ISO 8601 creation timestamp. |
Content types: application/json → application/json
cURL example
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/projects/c3d4e5f6-a7b8-9012-cdef-123456789012/api-keysResponse example
[
{
"id": "key-001",
"name": "CI Pipeline",
"key_prefix": "bo_live_abcd",
"scopes": [
"jobs:read",
"queues:execute"
],
"last_used_at": "2026-04-04T16:30:00Z",
"created_at": "2026-04-04T17:00:00Z"
}
]DELETE/api/api-keys/{keyId}Revoke API keyUser token
Permanently revokes an API key. Any requests using this key will immediately fail authentication.
# DELETE /api/api-keys/{keyId}Path parameters
| Field | Type / status | Required | Description |
|---|---|---|---|
keyId | string | Required | UUID of the API key to revoke. |
Response body
| Field | Type / status | Required | Description |
|---|---|---|---|
message | string | Required | Confirmation message. |
Content types: application/json → application/json
cURL example
curl -X DELETE -H "Authorization: Bearer YOUR_JWT_TOKEN" \
http://localhost:8080/api/api-keys/key-001Response example
{
"message": "API key revoked"
}