Documentation
Documentation/Integrations

API reference

Public endpoint schemas, authentication, scopes, and request examples.

APIRESTHTTP
View Markdown
On this page
Open the authenticated API explorer to send requests

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.

Authentication: none · Scopes: —

# GET /health

Response body

FieldType / statusRequiredDescription
bodystringRequiredPlain-text "ok" response.

Content types: application/json → application/json

cURL example

curl http://localhost:8080/health

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

Authentication: jwt_only · Scopes: read

# GET /api/workers/status

Query parameters

FieldType / statusRequiredDescription
project_iduuidRequiredProject whose worker availability should be returned.

Response body

FieldType / statusRequiredDescription
connectednumberRequiredNumber of workers visible for the requested project.
workersWorkerInfo[]RequiredArray of project-visible worker details.
workers[].worker_idstringRequiredUnique worker identifier for project-busy workers, or an anonymized availability identifier for idle workers.
workers[].statusstringRequiredWorker status: "idle" or "busy".
workers[].connected_atstringOptionalISO 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.

Authentication: jwt_only · Scopes: read

# GET /api/workspaces

Response body

FieldType / statusRequiredDescription
workspace_idstringRequiredWorkspace UUID from workspace_members.
rolestringRequiredUser role in the workspace.
workspacesobjectRequiredEmbedded 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/workspaces

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

Authentication: jwt_only · Scopes: write

# POST /api/workspaces

Request body

FieldType / statusRequiredDescription
namestringRequiredDisplay name for the new workspace.
max_concurrent_executionsnumberOptionalOptional workspace-wide running execution limit.

Response body

FieldType / statusRequiredDescription
idstringRequiredUUID of the newly created workspace.
namestringRequiredWorkspace name.
created_atstringRequiredISO 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/workspaces

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

Authentication: jwt_only · Scopes: write

# PUT /api/workspaces/{workspaceId}

Path parameters

FieldType / statusRequiredDescription
workspaceIdstringRequiredUUID of the workspace.

Request body

FieldType / statusRequiredDescription
namestringOptionalNew display name.
max_concurrent_executionsnumberOptionalWorkspace-wide running execution limit.

Response body

FieldType / statusRequiredDescription
idstringRequiredWorkspace UUID.
namestringRequiredWorkspace name.
max_concurrent_executionsnumberRequiredWorkspace-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-f12345678901

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

Authentication: jwt_only · Scopes: read

# GET /api/workspaces/{workspaceId}/stats

Path parameters

FieldType / statusRequiredDescription
workspaceIdstringRequiredUUID of the workspace.

Response body

FieldType / statusRequiredDescription
project_countnumberRequiredProjects in the workspace.
total_runsnumberRequiredTotal executions.
passednumberRequiredPassed executions.
failednumberRequiredFailed executions.
errorsnumberRequiredErrored executions.
runningnumberRequiredRunning or pending executions.
avg_duration_msnumberRequiredAverage completed run duration.
last_run_atstring | nullRequiredMost recent execution creation time.
pass_ratenumberRequiredPass percentage across terminal runs.
job_countnumberRequiredSaved 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/stats

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

Authentication: jwt_only · Scopes: read

# GET /api/workspaces/{workspaceId}/project-health

Path parameters

FieldType / statusRequiredDescription
workspaceIdstringRequiredUUID of the workspace.

Response body

FieldType / statusRequiredDescription
project_idstringRequiredAccessible project UUID.
recent_runsarrayRequiredUp to 12 recent status and creation-time records, oldest first.
recent_pass_ratenumberRequiredPass percentage across recent terminal runs.
runningnumberRequiredAll 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-health

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

Authentication: jwt_only · Scopes: —

# GET /api/workspaces/{workspaceId}/projects

Path parameters

FieldType / statusRequiredDescription
workspaceIdstringRequiredUUID of the workspace.

Response body

FieldType / statusRequiredDescription
idstringRequiredProject UUID.
namestringRequiredProject name.
created_atstringRequiredISO 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/projects

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

Authentication: jwt_only · Scopes: —

# POST /api/workspaces/{workspaceId}/projects

Path parameters

FieldType / statusRequiredDescription
workspaceIdstringRequiredUUID of the workspace.

Request body

FieldType / statusRequiredDescription
namestringRequiredProject name.
max_concurrent_executionsnumberOptionalOptional project-level running execution limit.

Response body

FieldType / statusRequiredDescription
idstringRequiredUUID of the newly created project.
namestringRequiredProject name.
workspace_idstringRequiredUUID of the owning workspace.
max_concurrent_executionsnumber | nullOptionalProject-level running execution limit, if set.
created_atstringRequiredISO 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/projects

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

Authentication: dual · Scopes: read

# GET /api/projects

Response body

FieldType / statusRequiredDescription
idstringRequiredProject UUID.
namestringRequiredProject name.
project_typestringRequiredProject type.
workspace_idstringRequiredUUID of the owning workspace.
created_atstringRequiredISO 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

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

Authentication: jwt_only · Scopes: write

# PUT /api/projects/{projectId}

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
namestringOptionalNew project name.
max_concurrent_executionsnumber | nullOptionalProject running execution limit, or null to inherit from the workspace.

Response body

FieldType / statusRequiredDescription
idstringRequiredProject UUID.
namestringRequiredProject name.
max_concurrent_executionsnumber | nullOptionalProject-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-123456789012

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

Authentication: none · Scopes: —

# GET /api/github/setup

Query parameters

FieldType / statusRequiredDescription
statestringRequiredSigned setup state returned by GitHub from the authorization or installation URL.
codestringOptionalGitHub OAuth code returned before installation so BernOps can obtain a user access token for setup verification.
installation_idnumberOptionalGitHub App installation ID assigned by GitHub after OAuth authorization completes.

Response body

FieldType / statusRequiredDescription
LocationstringRequiredFrontend 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-url

Response example

HTTP/1.1 302 Found
Location: http://app.bernops-dev/projects/github?projectId=PROJECT_ID&github=installed
POST/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.

Authentication: none · Scopes: —

# POST /api/github/webhooks

Request body

FieldType / statusRequiredDescription
payloadGitHub webhook JSONRequiredRaw GitHub webhook payload. The exact shape depends on the X-GitHub-Event header.

Response body

FieldType / statusRequiredDescription
acceptedbooleanRequiredWhether the delivery was accepted for processing.
delivery_idstringRequiredGitHub delivery ID from X-GitHub-Delivery.
statusstringOptionalProcessing 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/webhooks

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

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/github/install-url

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project to connect.

Response body

FieldType / statusRequiredDescription
urlstringRequiredGitHub App user authorization URL containing the signed state.
expires_atstringRequiredISO 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-url

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

Authentication: jwt_only · Scopes: —

# GET /api/projects/{projectId}/github/status

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
installedbooleanRequiredWhether the workspace has at least one GitHub App installation.
connectedbooleanRequiredWhether the project has a selected repository.
installationsGitHubInstallation[]RequiredWorkspace installations visible to the project.
repositoryGitHubRepository | nullOptionalSelected project repository, if connected.
pull_requestsGitHubPullRequest[]RequiredTracked BernOps export pull requests for the selected repository.
can_managebooleanRequiredWhether the current user can manage the GitHub connection.
can_installbooleanRequiredWhether the current user can create workspace-level GitHub App install URLs.
default_folderstringRequiredDefault 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/status

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

Authentication: jwt_only · Scopes: —

# DELETE /api/projects/{projectId}/github

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
installedbooleanRequiredWhether a workspace installation remains available.
connectedbooleanRequiredWhether the project still has a selected repository.
installationsGitHubInstallation[]RequiredWorkspace installations still available after disconnecting.
repositoryGitHubRepository | nullOptionalSelected repository after disconnecting, normally null.
pull_requestsGitHubPullRequest[]RequiredTracked pull requests after disconnecting, normally empty.
can_managebooleanRequiredWhether the current user can manage the GitHub connection.
can_installbooleanRequiredWhether the current user can create workspace-level GitHub App install URLs.
default_folderstringRequiredDefault 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/github

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

Authentication: jwt_only · Scopes: —

# GET /api/projects/{projectId}/github/repos

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
installationsArray<{ installation: GitHubInstallation; repositories: GitHubRepository[] }>RequiredRepositories 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/repos

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

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/github/repos/select

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
installation_idstringRequiredBernOps installation row UUID returned by repository listing.
github_repo_idnumberRequiredGitHub repository ID to connect.
test_folder_pathstringOptionalRepository folder for BernOps test exports. Defaults to bernops/e2e.

Response body

FieldType / statusRequiredDescription
connectedbooleanRequiredWhether the project now has a selected repository.
installedbooleanRequiredWhether the workspace has a GitHub App installation.
installationsGitHubInstallation[]RequiredWorkspace installations visible to the project.
repositoryGitHubRepositoryRequiredStored repository connection.
pull_requestsGitHubPullRequest[]RequiredTracked BernOps pull requests for the selected repository.
can_managebooleanRequiredWhether the current user can manage the GitHub connection.
can_installbooleanRequiredWhether the current user can create workspace-level GitHub App install URLs.
default_folderstringRequiredDefault 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/select

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

Authentication: jwt_only · Scopes: —

# GET /api/projects/{projectId}/github/tree

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Query parameters

FieldType / statusRequiredDescription
refstringOptionalBranch, tag, or SHA to read. Defaults to the selected repository default branch.
recursivebooleanOptionalWhether GitHub should return the tree recursively.

Response body

FieldType / statusRequiredDescription
shastringRequiredTree SHA returned by GitHub.
entriesGitHubTreeEntry[]RequiredRepository tree entries.
truncatedbooleanOptionalWhether 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.

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/github/export-tests

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
pull_requestGitHubPullRequestRequiredCreated or updated GitHub pull request.
commitGitHubCommitRequiredCommit created on the BernOps export branch.
exportedArray<{ job_id: string; path: string }>RequiredTests written to GitHub.
skippedArray<{ job_id: string; path?: string; reason: string }>RequiredTests 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-tests

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

Authentication: jwt_only · Scopes: —

# GET /api/projects/{projectId}/github/pull-requests

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
pull_requestsGitHubPullRequest[]RequiredTracked 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-requests

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

Authentication: jwt_only · Scopes: read

# GET /api/projects/{projectId}/variables

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
idstringRequiredVariable UUID.
namestringRequiredEnvironment variable name.
is_secretbooleanRequiredWhether the value is encrypted and write-only.
valuestring | nullOptionalPlain 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/variables

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

Authentication: jwt_only · Scopes: write

# POST /api/projects/{projectId}/variables

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
namestringRequiredUppercase env var name.
descriptionstringOptionalOperator-facing description.
is_secretbooleanRequiredEncrypt and hide the value after save.
valuestringRequiredVariable value to store.

Response body

FieldType / statusRequiredDescription
idstringRequiredVariable UUID.
namestringRequiredEnvironment variable name.
is_secretbooleanRequiredWhether the value is secret.
secret_versionnumberRequiredSecret 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/variables

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

Authentication: jwt_only · Scopes: write

# PUT /api/project-variables/{variableId}

Path parameters

FieldType / statusRequiredDescription
variableIdstringRequiredUUID of the variable.

Request body

FieldType / statusRequiredDescription
namestringOptionalNew uppercase env var name.
descriptionstringOptionalNew description.
valuestringOptionalNew value.

Response body

FieldType / statusRequiredDescription
idstringRequiredVariable UUID.
namestringRequiredEnvironment variable name.
secret_versionnumberRequiredSecret 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-002

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

Authentication: jwt_only · Scopes: write

# DELETE /api/project-variables/{variableId}

Path parameters

FieldType / statusRequiredDescription
variableIdstringRequiredUUID of the variable.

Response body

FieldType / statusRequiredDescription
deletedbooleanRequiredDeletion 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-002

Response example

{
  "deleted": true
}
POST/api/project-variables/{variableId}/rotateRotate secret variableUser token

Replaces the value for a secret variable and increments its version.

Authentication: jwt_only · Scopes: write

# POST /api/project-variables/{variableId}/rotate

Path parameters

FieldType / statusRequiredDescription
variableIdstringRequiredUUID of the secret variable.

Request body

FieldType / statusRequiredDescription
valuestringRequiredNew secret value.

Response body

FieldType / statusRequiredDescription
idstringRequiredVariable UUID.
secret_versionnumberRequiredIncremented secret version.
last_rotated_atstringRequiredRotation 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/rotate

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

Authentication: dual · Scopes: jobs:read

# GET /api/projects/{projectId}/jobs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
idstringRequiredJob UUID.
namestringRequiredJob display name.
descriptionstringOptionalOptional description.
file_namestringOptionalOriginal file name.
created_atstringRequiredISO 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/jobs

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

Authentication: dual · Scopes: jobs:write

# POST /api/projects/{projectId}/jobs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
namestringRequiredJob display name.
descriptionstringOptionalOptional description of what the job tests.
codestringRequiredPlaywright test source code.
file_namestringOptionalOriginal file name (e.g. login.spec.ts).
max_concurrent_executionsnumber | nullOptionalOptional per-job concurrency limit. Null inherits project and workspace limits.

Response body

FieldType / statusRequiredDescription
idstringRequiredUUID of the newly created job.
namestringRequiredJob name.
codestringRequiredJob source code.
created_atstringRequiredISO 8601 creation timestamp.
max_concurrent_executionsnumber | nullOptionalPer-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/jobs

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

Authentication: dual · Scopes: jobs:read

# GET /api/jobs/{jobId}

Path parameters

FieldType / statusRequiredDescription
jobIdstringRequiredUUID of the job.

Response body

FieldType / statusRequiredDescription
idstringRequiredJob UUID.
namestringRequiredJob name.
descriptionstringOptionalOptional description.
codestringRequiredPlaywright test source code.
file_namestringOptionalOriginal file name.
created_atstringRequiredISO 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-345678901234

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

Authentication: dual · Scopes: jobs:write

# PUT /api/jobs/{jobId}

Path parameters

FieldType / statusRequiredDescription
jobIdstringRequiredUUID of the job.

Request body

FieldType / statusRequiredDescription
namestringOptionalNew job name.
descriptionstringOptionalNew description.
codestringOptionalUpdated Playwright test source code.
file_namestringOptionalUpdated file name.
max_concurrent_executionsnumber | nullOptionalSet or clear the per-job concurrency limit.

Response body

FieldType / statusRequiredDescription
idstringRequiredJob UUID.
namestringRequiredUpdated job name.
codestringRequiredUpdated source code.
max_concurrent_executionsnumber | nullOptionalPer-script concurrency limit, when configured.
updated_atstringRequiredISO 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-345678901234

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

Authentication: dual · Scopes: jobs:write

# DELETE /api/jobs/{jobId}

Path parameters

FieldType / statusRequiredDescription
jobIdstringRequiredUUID of the job to delete.

Response body

FieldType / statusRequiredDescription
messagestringRequiredConfirmation 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-345678901234

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

Authentication: dual · Scopes: runs:read

# GET /api/jobs/{jobId}/runs

Path parameters

FieldType / statusRequiredDescription
jobIdstringRequiredUUID of the job.

Response body

FieldType / statusRequiredDescription
idstringRequiredTest run execution ID.
statusstringRequiredRun status: "pending", "running", "passed", "failed", "cancelled".
started_atstringOptionalISO 8601 start timestamp.
completed_atstringOptionalISO 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/runs

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

Authentication: dual · Scopes: jobs:execute

# POST /api/jobs/{jobId}/execute

Path parameters

FieldType / statusRequiredDescription
jobIdstringRequiredUUID of the saved job to execute.

Request body

FieldType / statusRequiredDescription
base_urlstringOptionalBase URL override for this run.
include_secret_variablesbooleanOptionalWhether to include secret project variables. Defaults to true for API keys with `variables:use`, false otherwise.

Response body

FieldType / statusRequiredDescription
execution_idstringRequiredUnique execution ID for this test run.
statusstringRequiredInitial status, typically "pending" or "running".
worker_idstringOptionalID 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/execute

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

Authentication: dual · Scopes: jobs:read

# GET /api/jobs/{jobId}/stats

Path parameters

FieldType / statusRequiredDescription
jobIdstringRequiredUUID of the job.

Response body

FieldType / statusRequiredDescription
total_runsnumberRequiredTotal number of runs for this job.
pass_ratenumberRequiredPercentage of passed runs (0-100).
avg_duration_msnumberRequiredAverage run duration in milliseconds.
last_run_atstringOptionalISO 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/stats

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

Authentication: dual · Scopes: runs:write jobs:execute

# POST /api/projects/{projectId}/test-runs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
test_filesTestFile[]OptionalInline Playwright files to execute. Each entry should include `file_name` and `code`.
base_urlstringOptionalBase URL override for this run.
job_idstringOptionalUUID of a saved job to execute. When present, `jobs:execute` is sufficient.

Response body

FieldType / statusRequiredDescription
execution_idstringRequiredUnique execution ID for this test run.
statusstringRequiredInitial status (typically "pending" or "running").
worker_idstringOptionalID 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-runs

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

Authentication: dual · Scopes: runs:read

# GET /api/projects/{projectId}/test-runs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
idstringRequiredTest run execution ID.
statusstringRequiredRun status.
job_idstringOptionalAssociated job UUID, if any.
started_atstringOptionalISO 8601 start timestamp.
completed_atstringOptionalISO 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-runs

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

Authentication: dual · Scopes: project:read

# GET /api/projects/{projectId}/stats

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
total_runsnumberRequiredTotal test runs in this project.
pass_ratenumberRequiredPercentage of passed runs (0-100).
passednumberRequiredPassed executions.
failednumberRequiredFailed executions.
errorsnumberRequiredErrored executions.
runningnumberRequiredPending or running executions.
avg_duration_msnumberRequiredAverage duration across executions that reported one.
last_run_atstring | nullRequiredMost recent execution creation time.
job_countnumberRequiredSaved 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/stats

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

Authentication: dual · Scopes: runs:read

# GET /api/projects/{projectId}/run-activity

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Query parameters

FieldType / statusRequiredDescription
daysintegerOptionalNumber of daily buckets to return, from 1 through 30. (Default: 7)
timezonestringOptionalIANA time zone used to determine each execution's calendar date. (Default: UTC)

Response body

FieldType / statusRequiredDescription
[].datestringRequiredCalendar date in YYYY-MM-DD format.
[].passednumberRequiredExecutions that passed on this date.
[].unsuccessfulnumberRequiredExecutions that failed or errored on this date.
[].activenumberRequiredExecutions 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.

Authentication: dual · Scopes: runs:read

# GET /api/projects/{projectId}/dashboard-runs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
recent_runsRunSummary[]RequiredAt most 60 runs ordered newest first.
latest_job_runsRunSummary[]RequiredLatest 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-runs

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

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution ID of the test run.

Response body

FieldType / statusRequiredDescription
[]TestExecution[]RequiredArray containing the matching execution record.
[].idstringRequiredTest run execution ID.
[].statusstringRequiredCurrent run status.
[].project_idstringRequiredUUID of the parent project.
[].progressProgressSnapshot | nullRequiredLatest 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-a1b2c3d4

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

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}/logs

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution ID of the test run.

Query parameters

FieldType / statusRequiredDescription
limitnumberOptionalMaximum number of log rows to return. (Default: 50)
offsetnumberOptionalPagination offset. (Default: 0)

Response body

FieldType / statusRequiredDescription
dataLogEntry[]RequiredArray of log entries.
totalnumberRequiredTotal number of log rows for the execution.
data[].timestampstringRequiredISO 8601 timestamp of the log entry.
data[].levelstringRequiredLog level: "info", "warn", "error", "debug".
data[].messagestringRequiredLog 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/logs

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

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}/steps

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution ID of the test run.

Query parameters

FieldType / statusRequiredDescription
limitnumberOptionalMaximum number of step rows to return. (Default: 50)
offsetnumberOptionalPagination offset. (Default: 0)

Response body

FieldType / statusRequiredDescription
dataTestStep[]RequiredArray of test steps.
totalnumberRequiredTotal number of stored step rows for the execution.
data[].eventstringRequiredStep event name such as test:start, step:start, or step:end.
data[].titlestringOptionalDisplay title for the step event.
data[].duration_msnumberOptionalStep 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/steps

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

Authentication: dual · Scopes: runs:finish

# POST /api/test-runs/{executionId}/cancel

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution ID of the test run to cancel.

Response body

FieldType / statusRequiredDescription
execution_idstringRequiredExecution ID of the cancelled run.
statusstringRequiredUpdated status ("error").
messagestringRequiredHuman-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/cancel

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

Authentication: dual · Scopes: external_jobs:read

# GET /api/projects/{projectId}/external-jobs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredProject UUID.

Response body

FieldType / statusRequiredDescription
idstringRequiredExternal job UUID.
namestringRequiredJob display name.
slugstringRequiredProject-unique slug.
retention_policyobjectRequiredCurrent 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-jobs

Response 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`.

Authentication: dual · Scopes: external_jobs:write

# POST /api/projects/{projectId}/external-jobs

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredProject UUID.

Request body

FieldType / statusRequiredDescription
namestringRequiredJob display name.
descriptionstringOptionalOptional description.
default_servicestringOptionalDefault service for future runs.
default_environmentstringOptionalDefault environment for future runs.

Response body

FieldType / statusRequiredDescription
jobobjectRequiredCreated external job.
api_keyobject | nullRequiredInitial API key metadata, or `null` for API-key-authenticated callers.
api_key.keystringOptionalPlaintext 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-jobs

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

Authentication: dual · Scopes: external_jobs:read

# GET /api/external-jobs/{externalJobId}

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Response body

FieldType / statusRequiredDescription
idstringRequiredExternal job UUID.
project_idstringRequiredOwning project UUID.
retention_policyobjectRequiredRetention 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-001

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

Authentication: dual · Scopes: external_jobs:write

# PUT /api/external-jobs/{externalJobId}

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Request body

FieldType / statusRequiredDescription
retention_policyobjectOptionalUpdated retention policy.

Response body

FieldType / statusRequiredDescription
idstringRequiredUpdated 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-001

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

Authentication: dual · Scopes: external_jobs:read

# GET /api/external-jobs/{externalJobId}/stats

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Response body

FieldType / statusRequiredDescription
total_runsnumberRequiredTotal 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/stats

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

Authentication: dual · Scopes: external_jobs:read

# GET /api/external-jobs/{externalJobId}/metrics

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Query parameters

FieldType / statusRequiredDescription
namestringOptionalMetric name to filter by.
fromstringOptionalInclusive ISO 8601 lower bound for event_at.
tostringOptionalInclusive ISO 8601 upper bound for event_at.

Response body

FieldType / statusRequiredDescription
namestringRequiredMetric 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.

Authentication: dual · Scopes: external_jobs:read

# GET /api/external-jobs/{externalJobId}/runs

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Query parameters

FieldType / statusRequiredDescription
limitnumberOptionalMaximum number of runs to return. (Default: 100)
offsetnumberOptionalPagination offset. (Default: 0)

Response body

FieldType / statusRequiredDescription
dataRun[]RequiredPaginated 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.

Authentication: dual · Scopes: external_jobs:read

# GET /api/external-jobs/{externalJobId}/cleanup-status

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Response body

FieldType / statusRequiredDescription
cleanupobjectOptionalLatest 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-status

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

Authentication: dual · Scopes: external_jobs:write

# POST /api/external-jobs/{externalJobId}/cleanup

Path parameters

FieldType / statusRequiredDescription
externalJobIdstringRequiredExternal job UUID.

Response body

FieldType / statusRequiredDescription
cleanup.statusstringRequiredCleanup 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/cleanup

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

Authentication: dual · Scopes: runs:write

# POST /api/ingest/runs

Request body

FieldType / statusRequiredDescription
external_job_idstringOptionalExternal job UUID. Optional when the API key is already job-scoped.
external_run_idstringOptionalCaller-defined idempotency key for the run.
display_namestringOptionalHuman-readable run title.

Response body

FieldType / statusRequiredDescription
execution_idstringRequiredCreated or reused execution ID.
duplicatebooleanRequiredWhether this call reused an existing run.
progress_sequencenumberRequiredLatest accepted producer sequence, or zero for a new run.
progress_revisionnumberRequiredLatest 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/runs

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

Authentication: dual · Scopes: logs:write

# POST /api/ingest/runs/{runId}/logs

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
event_idstringOptionalIdempotency key for this log.
messagestringRequiredLog message.

Response body

FieldType / statusRequiredDescription
acceptedbooleanRequiredWhether 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/logs

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

Authentication: dual · Scopes: steps:write

# POST /api/ingest/runs/{runId}/steps

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
event_idstringOptionalIdempotency key for this step event.
eventstringRequiredEvent name such as step:start or step:end.
titlestringOptionalDisplay title for the step.

Response body

FieldType / statusRequiredDescription
acceptedbooleanRequiredWhether 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/steps

Response example

{
  "accepted": true,
  "duplicate": false
}
POST/api/ingest/runs/{runId}/metricsAppend metric pointUser token or API key

Stores one metric sample for the execution.

Authentication: dual · Scopes: metrics:write

# POST /api/ingest/runs/{runId}/metrics

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
event_idstringOptionalIdempotency key for this metric sample.
namestringRequiredMetric name.
kindstringRequiredgauge, count, rate, or duration.
valuenumberRequiredMetric value.

Response body

FieldType / statusRequiredDescription
acceptedbooleanRequiredWhether 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/metrics

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

Authentication: dual · Scopes: progress:write

# POST /api/ingest/runs/{runId}/progress

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
event_idstringRequiredStable idempotency key for this progress report.
sequencenumberRequiredPositive increasing producer sequence.
currentnumberOptionalCurrent count; provide with total only.
totalnumberOptionalPositive total count; provide with current only.
percentnumberOptionalPercent-only progress from 0 through 100.
stagestringOptionalCurrent stage, up to 128 characters.
messagestringOptionalProgress message, up to 1024 characters.

Response body

FieldType / statusRequiredDescription
acceptedbooleanRequiredWhether the report was accepted.
duplicatebooleanRequiredWhether this was an exact retry.
progressProgressSnapshotRequiredCanonical 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/progress

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

Authentication: dual · Scopes: attachments:write

# POST /api/ingest/runs/{runId}/attachments

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
filemultipart fileRequiredSupported file to upload.
event_idstringOptionalIdempotency key for this attachment.

Response body

FieldType / statusRequiredDescription
attachment_idstringRequiredCreated 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/attachments

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

Authentication: dual · Scopes: runs:write

# POST /api/ingest/runs/{runId}/heartbeat

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
heartbeat_atstringOptionalOptional ISO 8601 heartbeat timestamp.

Response body

FieldType / statusRequiredDescription
last_heartbeat_atstringRequiredStored 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/heartbeat

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

Authentication: dual · Scopes: runs:finish

# POST /api/ingest/runs/{runId}/complete

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
finished_atstringOptionalISO 8601 finish timestamp.
duration_msnumberOptionalExecution duration in milliseconds.

Response body

FieldType / statusRequiredDescription
statusstringRequiredFinal 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/complete

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

Authentication: dual · Scopes: runs:finish

# POST /api/ingest/runs/{runId}/fail

Path parameters

FieldType / statusRequiredDescription
runIdstringRequiredExecution UUID returned by start run.

Request body

FieldType / statusRequiredDescription
errorstringRequiredError message describing the failure.

Response body

FieldType / statusRequiredDescription
statusstringRequiredFinal 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/fail

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

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}/metrics

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution UUID.

Query parameters

FieldType / statusRequiredDescription
limitnumberOptionalMaximum records to return. (Default: 100)
offsetnumberOptionalPagination offset. (Default: 0)

Response body

FieldType / statusRequiredDescription
dataMetric[]RequiredPaginated 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.

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}/progress

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution UUID.

Query parameters

FieldType / statusRequiredDescription
limitnumberOptionalMaximum records to return. (Default: 50)
offsetnumberOptionalPagination offset. (Default: 0)

Response body

FieldType / statusRequiredDescription
dataProgressSnapshot[]RequiredCanonical progress history.
totalnumberRequiredTotal 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.

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}/attachments

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution UUID.

Query parameters

FieldType / statusRequiredDescription
limitnumberOptionalMaximum records to return. (Default: 50)
offsetnumberOptionalPagination offset. (Default: 0)

Response body

FieldType / statusRequiredDescription
dataAttachment[]RequiredPaginated 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.

Authentication: dual · Scopes: runs:read

# GET /api/test-runs/{executionId}/attachments/{attachmentId}/download

Path parameters

FieldType / statusRequiredDescription
executionIdstringRequiredExecution UUID.
attachmentIdstringRequiredAttachment UUID.

Response body

FieldType / statusRequiredDescription
attachment_idstringRequiredAttachment UUID.
file_namestringRequiredAttachment file name.
content_typestringRequiredAttachment content type.
download_urlstringRequiredShort-lived signed download URL.
expires_atstringRequiredISO 8601 expiration timestamp for the signed URL.
expires_in_secondsnumberRequiredSigned 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/download

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

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.

Query parameters

FieldType / statusRequiredDescription
qstringOptionalTrimmed full-text query, at most 500 Unicode characters.
archiveactive | archived | allOptionalArchive-state filter. (Default: active)
limitintegerOptionalMaximum page summaries, from 1 through 100. (Default: 50)
offsetintegerOptionalZero-based result offset, from 0 through 10000. (Default: 0)

Response body

FieldType / statusRequiredDescription
pagesWikiPageSummary[]RequiredSummaries with id, project_id, title, slug, filename, revision, archived_at, updated_at, and numeric rank; body_markdown is intentionally omitted.
qstringRequiredNormalized query.
archiveactive | archived | allRequiredApplied archive filter.
limitintegerRequiredApplied limit.
offsetintegerRequiredApplied offset.
capabilities.can_createbooleanRequiredWhether this principal can create pages.
capabilities.can_exportbooleanRequiredAlways true after project read authorization.
capabilities.can_importbooleanRequiredTrue only for an authorized JWT project administrator.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
invalid_wiki_search400Optionalq, 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.

Authentication: dual · Scopes: wiki:write

# POST /api/projects/{projectId}/wiki/pages

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.

Request headers

FieldType / statusRequiredDescription
Idempotency-KeystringOptionalRequired 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

FieldType / statusRequiredDescription
titlestringRequiredTitle normalized by trimming; final length is 1-300 characters.
slugstringOptionalSource for a lowercase kebab-case slug, at most 200 characters. Defaults to title.
filenamestringOptionalSource for a lowercase relative .md path, 3-512 characters. Defaults to {slug}.md.
body_markdownstringOptionalUTF-8 Markdown; canonical result is at most 1 MiB.

Response headers

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.

Response body

FieldType / statusRequiredDescription
outcomeapplied | noopRequiredWhether this command changed the page.
codestringRequiredStable success/no-op code for this command.
page.idUUIDRequiredPage UUID.
page.project_idUUIDRequiredOwning project UUID.
page.titlestringRequiredTrimmed title, 1-300 characters.
page.slugstringRequiredLowercase kebab-case slug, at most 200 characters.
page.normalized_slugstringRequiredStored normalized slug used for project uniqueness.
page.filenamestringRequiredLowercase project-relative .md filename, 3-512 characters.
page.normalized_filenamestringRequiredStored normalized filename used for project uniqueness.
page.body_markdownstringRequiredCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.
page.revisionintegerRequiredCurrent positive revision, up to 9007199254740991.
page.archived_atISO 8601 string | nullRequiredArchive timestamp, or null while active.
page.created_actor_kinduser | api_key | systemRequiredPrincipal kind that created the page.
page.created_actor_user_idUUIDOptionalCreating user ID when actor_kind is user.
page.created_actor_api_key_idUUIDOptionalCreating API key ID when actor_kind is api_key.
page.created_actor_displaystring | nullRequiredStored display identity for the creating actor.
page.updated_actor_kinduser | api_key | systemRequiredPrincipal kind that last changed the page.
page.updated_actor_user_idUUIDOptionalLast updating user ID when actor_kind is user.
page.updated_actor_api_key_idUUIDOptionalLast updating API key ID when actor_kind is api_key.
page.updated_actor_displaystring | nullRequiredStored display identity for the last updating actor.
page.created_atISO 8601 stringRequiredCreation timestamp.
page.updated_atISO 8601 stringRequiredLast update timestamp.
audit_event_idUUID | nullRequiredAppend-only audit event ID, or null for a no-op.
source_revisionintegerOptionalSource revision on a restore result.
replayedbooleanRequiredTrue when an exact Idempotency-Key retry replayed this result.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
invalid_json_body400OptionalThe body is not one JSON value, contains unknown fields, or has invalid field types.
request_body_too_large413OptionalThe JSON request exceeds the route's byte limit.
invalid_idempotency_key400OptionalAn API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters.
idempotency_mismatch409OptionalThe same actor already used this Idempotency-Key for different canonical command input.
invalid_title400OptionalThe normalized title is empty or longer than 300 characters.
invalid_slug400OptionalThe normalized slug is empty, longer than 200 characters, or cannot match lowercase kebab-case.
invalid_filename400OptionalThe normalized Markdown filename is invalid, hidden, non-canonical, or longer than 512 characters.
markdown_too_large413OptionalCanonical UTF-8 Markdown exceeds 1 MiB.
wiki_slug_conflict409OptionalAnother project wiki page already uses the normalized slug.
wiki_filename_conflict409OptionalAnother project wiki page already uses the normalized filename.
revision_exhausted409OptionalThe page reached revision 9007199254740991.
wiki_attachment_reference_not_found409OptionalMarkdown references an attachment that does not exist on this project page.
invalid_create_input400Optionaltitle 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/pages

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

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages/{pageId}

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Response headers

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.

Response body

FieldType / statusRequiredDescription
page.idUUIDRequiredPage UUID.
page.project_idUUIDRequiredOwning project UUID.
page.titlestringRequiredTrimmed title, 1-300 characters.
page.slugstringRequiredLowercase kebab-case slug, at most 200 characters.
page.normalized_slugstringRequiredStored normalized slug used for project uniqueness.
page.filenamestringRequiredLowercase project-relative .md filename, 3-512 characters.
page.normalized_filenamestringRequiredStored normalized filename used for project uniqueness.
page.body_markdownstringRequiredCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.
page.revisionintegerRequiredCurrent positive revision, up to 9007199254740991.
page.archived_atISO 8601 string | nullRequiredArchive timestamp, or null while active.
page.created_actor_kinduser | api_key | systemRequiredPrincipal kind that created the page.
page.created_actor_user_idUUIDOptionalCreating user ID when actor_kind is user.
page.created_actor_api_key_idUUIDOptionalCreating API key ID when actor_kind is api_key.
page.created_actor_displaystring | nullRequiredStored display identity for the creating actor.
page.updated_actor_kinduser | api_key | systemRequiredPrincipal kind that last changed the page.
page.updated_actor_user_idUUIDOptionalLast updating user ID when actor_kind is user.
page.updated_actor_api_key_idUUIDOptionalLast updating API key ID when actor_kind is api_key.
page.updated_actor_displaystring | nullRequiredStored display identity for the last updating actor.
page.created_atISO 8601 stringRequiredCreation timestamp.
page.updated_atISO 8601 stringRequiredLast update timestamp.
capabilities.can_readbooleanRequiredTrue after read authorization.
capabilities.can_exportbooleanRequiredTrue after read authorization.
capabilities.can_updatebooleanRequiredTrue for a mutable active page and a mutation-capable principal.
capabilities.can_restorebooleanRequiredTrue for a mutation-capable principal, including on archived pages.
capabilities.can_archivebooleanRequiredTrue only for a JWT administrator and an active page.
capabilities.can_unarchivebooleanRequiredTrue only for a JWT administrator and an archived page.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe 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-000000000201

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

Authentication: dual · Scopes: wiki:read wiki:write

# PUT /api/projects/{projectId}/wiki/pages/{pageId}

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Request headers

FieldType / statusRequiredDescription
If-Matchstrong ETagRequiredExact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected.
Idempotency-KeystringOptionalRequired 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

FieldType / statusRequiredDescription
titlestringOptionalTrimmed title, 1-300 characters.
slugstringOptionalLowercase kebab-case slug, at most 200 characters.
filenamestringOptionalLowercase project-relative .md filename, 3-512 characters.
body_markdownstringOptionalCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.

Response headers

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.

Response body

FieldType / statusRequiredDescription
outcomeapplied | noopRequiredWhether this command changed the page.
codestringRequiredStable success/no-op code for this command.
page.idUUIDRequiredPage UUID.
page.project_idUUIDRequiredOwning project UUID.
page.titlestringRequiredTrimmed title, 1-300 characters.
page.slugstringRequiredLowercase kebab-case slug, at most 200 characters.
page.normalized_slugstringRequiredStored normalized slug used for project uniqueness.
page.filenamestringRequiredLowercase project-relative .md filename, 3-512 characters.
page.normalized_filenamestringRequiredStored normalized filename used for project uniqueness.
page.body_markdownstringRequiredCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.
page.revisionintegerRequiredCurrent positive revision, up to 9007199254740991.
page.archived_atISO 8601 string | nullRequiredArchive timestamp, or null while active.
page.created_actor_kinduser | api_key | systemRequiredPrincipal kind that created the page.
page.created_actor_user_idUUIDOptionalCreating user ID when actor_kind is user.
page.created_actor_api_key_idUUIDOptionalCreating API key ID when actor_kind is api_key.
page.created_actor_displaystring | nullRequiredStored display identity for the creating actor.
page.updated_actor_kinduser | api_key | systemRequiredPrincipal kind that last changed the page.
page.updated_actor_user_idUUIDOptionalLast updating user ID when actor_kind is user.
page.updated_actor_api_key_idUUIDOptionalLast updating API key ID when actor_kind is api_key.
page.updated_actor_displaystring | nullRequiredStored display identity for the last updating actor.
page.created_atISO 8601 stringRequiredCreation timestamp.
page.updated_atISO 8601 stringRequiredLast update timestamp.
audit_event_idUUID | nullRequiredAppend-only audit event ID, or null for a no-op.
source_revisionintegerOptionalSource revision on a restore result.
replayedbooleanRequiredTrue when an exact Idempotency-Key retry replayed this result.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
invalid_json_body400OptionalThe body is not one JSON value, contains unknown fields, or has invalid field types.
request_body_too_large413OptionalThe JSON request exceeds the route's byte limit.
invalid_idempotency_key400OptionalAn API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters.
idempotency_mismatch409OptionalThe same actor already used this Idempotency-Key for different canonical command input.
invalid_title400OptionalThe normalized title is empty or longer than 300 characters.
invalid_slug400OptionalThe normalized slug is empty, longer than 200 characters, or cannot match lowercase kebab-case.
invalid_filename400OptionalThe normalized Markdown filename is invalid, hidden, non-canonical, or longer than 512 characters.
markdown_too_large413OptionalCanonical UTF-8 Markdown exceeds 1 MiB.
wiki_slug_conflict409OptionalAnother project wiki page already uses the normalized slug.
wiki_filename_conflict409OptionalAnother project wiki page already uses the normalized filename.
revision_exhausted409OptionalThe page reached revision 9007199254740991.
wiki_attachment_reference_not_found409OptionalMarkdown references an attachment that does not exist on this project page.
if_match_required428OptionalIf-Match is required for this page command.
invalid_if_match400OptionalIf-Match is not the exact strong ETag for this page.
revision_conflict412OptionalThe page has changed. The error details include expected_revision, current_revision, and current_etag; the response ETag is current.
invalid_update_changes400OptionalA change is unknown, null, or not a string.
wiki_page_archived409OptionalArchived 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-000000000201

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

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/wiki/pages/{pageId}/archive

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Request headers

FieldType / statusRequiredDescription
If-Matchstrong ETagRequiredExact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected.
Idempotency-KeystringOptionalRequired 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

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.

Response body

FieldType / statusRequiredDescription
outcomeapplied | noopRequiredWhether this command changed the page.
codestringRequiredStable success/no-op code for this command.
page.idUUIDRequiredPage UUID.
page.project_idUUIDRequiredOwning project UUID.
page.titlestringRequiredTrimmed title, 1-300 characters.
page.slugstringRequiredLowercase kebab-case slug, at most 200 characters.
page.normalized_slugstringRequiredStored normalized slug used for project uniqueness.
page.filenamestringRequiredLowercase project-relative .md filename, 3-512 characters.
page.normalized_filenamestringRequiredStored normalized filename used for project uniqueness.
page.body_markdownstringRequiredCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.
page.revisionintegerRequiredCurrent positive revision, up to 9007199254740991.
page.archived_atISO 8601 string | nullRequiredArchive timestamp, or null while active.
page.created_actor_kinduser | api_key | systemRequiredPrincipal kind that created the page.
page.created_actor_user_idUUIDOptionalCreating user ID when actor_kind is user.
page.created_actor_api_key_idUUIDOptionalCreating API key ID when actor_kind is api_key.
page.created_actor_displaystring | nullRequiredStored display identity for the creating actor.
page.updated_actor_kinduser | api_key | systemRequiredPrincipal kind that last changed the page.
page.updated_actor_user_idUUIDOptionalLast updating user ID when actor_kind is user.
page.updated_actor_api_key_idUUIDOptionalLast updating API key ID when actor_kind is api_key.
page.updated_actor_displaystring | nullRequiredStored display identity for the last updating actor.
page.created_atISO 8601 stringRequiredCreation timestamp.
page.updated_atISO 8601 stringRequiredLast update timestamp.
audit_event_idUUID | nullRequiredAppend-only audit event ID, or null for a no-op.
source_revisionintegerOptionalSource revision on a restore result.
replayedbooleanRequiredTrue when an exact Idempotency-Key retry replayed this result.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_admin_required403OptionalThe operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it.
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
invalid_json_body400OptionalThe body is not one JSON value, contains unknown fields, or has invalid field types.
request_body_too_large413OptionalThe JSON request exceeds the route's byte limit.
invalid_idempotency_key400OptionalAn API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters.
idempotency_mismatch409OptionalThe same actor already used this Idempotency-Key for different canonical command input.
if_match_required428OptionalIf-Match is required for this page command.
invalid_if_match400OptionalIf-Match is not the exact strong ETag for this page.
revision_conflict412OptionalThe 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/archive

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

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/wiki/pages/{pageId}/unarchive

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Request headers

FieldType / statusRequiredDescription
If-Matchstrong ETagRequiredExact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected.
Idempotency-KeystringOptionalRequired 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

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.

Response body

FieldType / statusRequiredDescription
outcomeapplied | noopRequiredWhether this command changed the page.
codestringRequiredStable success/no-op code for this command.
page.idUUIDRequiredPage UUID.
page.project_idUUIDRequiredOwning project UUID.
page.titlestringRequiredTrimmed title, 1-300 characters.
page.slugstringRequiredLowercase kebab-case slug, at most 200 characters.
page.normalized_slugstringRequiredStored normalized slug used for project uniqueness.
page.filenamestringRequiredLowercase project-relative .md filename, 3-512 characters.
page.normalized_filenamestringRequiredStored normalized filename used for project uniqueness.
page.body_markdownstringRequiredCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.
page.revisionintegerRequiredCurrent positive revision, up to 9007199254740991.
page.archived_atISO 8601 string | nullRequiredArchive timestamp, or null while active.
page.created_actor_kinduser | api_key | systemRequiredPrincipal kind that created the page.
page.created_actor_user_idUUIDOptionalCreating user ID when actor_kind is user.
page.created_actor_api_key_idUUIDOptionalCreating API key ID when actor_kind is api_key.
page.created_actor_displaystring | nullRequiredStored display identity for the creating actor.
page.updated_actor_kinduser | api_key | systemRequiredPrincipal kind that last changed the page.
page.updated_actor_user_idUUIDOptionalLast updating user ID when actor_kind is user.
page.updated_actor_api_key_idUUIDOptionalLast updating API key ID when actor_kind is api_key.
page.updated_actor_displaystring | nullRequiredStored display identity for the last updating actor.
page.created_atISO 8601 stringRequiredCreation timestamp.
page.updated_atISO 8601 stringRequiredLast update timestamp.
audit_event_idUUID | nullRequiredAppend-only audit event ID, or null for a no-op.
source_revisionintegerOptionalSource revision on a restore result.
replayedbooleanRequiredTrue when an exact Idempotency-Key retry replayed this result.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_admin_required403OptionalThe operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it.
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
invalid_json_body400OptionalThe body is not one JSON value, contains unknown fields, or has invalid field types.
request_body_too_large413OptionalThe JSON request exceeds the route's byte limit.
invalid_idempotency_key400OptionalAn API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters.
idempotency_mismatch409OptionalThe same actor already used this Idempotency-Key for different canonical command input.
if_match_required428OptionalIf-Match is required for this page command.
invalid_if_match400OptionalIf-Match is not the exact strong ETag for this page.
revision_conflict412OptionalThe 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/unarchive

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

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages/{pageId}/revisions

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Query parameters

FieldType / statusRequiredDescription
limitintegerOptionalMaximum revision summaries, from 1 through 100. (Default: 50)
offsetintegerOptionalZero-based revision offset, from 0 through 10000. (Default: 0)

Response body

FieldType / statusRequiredDescription
revisionsWikiRevisionSummary[]RequiredRows 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_idUUIDRequiredRequested page UUID.
limitintegerRequiredApplied limit.
offsetintegerRequiredApplied offset.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
invalid_revision_query400Optionallimit 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.

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages/{pageId}/revisions/{revision}

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.
revisionintegerRequiredCanonical positive revision number, without leading zeroes.

Response body

FieldType / statusRequiredDescription
revisionWikiRevisionRequiredImmutable 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

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
wiki_revision_not_found404OptionalThe 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/1

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

Authentication: dual · Scopes: wiki:read wiki:write

# POST /api/projects/{projectId}/wiki/pages/{pageId}/restore

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Request headers

FieldType / statusRequiredDescription
If-Matchstrong ETagRequiredExact current page ETag: "wiki-page:{pageId}:rev:{revision}". Weak tags, *, lists, another page ID, and non-canonical revision numbers are rejected.
Idempotency-KeystringOptionalRequired 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

FieldType / statusRequiredDescription
source_revisionintegerRequiredExisting positive revision to copy, up to 9007199254740991.

Response headers

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.

Response body

FieldType / statusRequiredDescription
outcomeapplied | noopRequiredWhether this command changed the page.
codestringRequiredStable success/no-op code for this command.
page.idUUIDRequiredPage UUID.
page.project_idUUIDRequiredOwning project UUID.
page.titlestringRequiredTrimmed title, 1-300 characters.
page.slugstringRequiredLowercase kebab-case slug, at most 200 characters.
page.normalized_slugstringRequiredStored normalized slug used for project uniqueness.
page.filenamestringRequiredLowercase project-relative .md filename, 3-512 characters.
page.normalized_filenamestringRequiredStored normalized filename used for project uniqueness.
page.body_markdownstringRequiredCanonical Markdown head, at most 1 MiB; CRLF/CR is normalized to LF and non-empty content ends with LF.
page.revisionintegerRequiredCurrent positive revision, up to 9007199254740991.
page.archived_atISO 8601 string | nullRequiredArchive timestamp, or null while active.
page.created_actor_kinduser | api_key | systemRequiredPrincipal kind that created the page.
page.created_actor_user_idUUIDOptionalCreating user ID when actor_kind is user.
page.created_actor_api_key_idUUIDOptionalCreating API key ID when actor_kind is api_key.
page.created_actor_displaystring | nullRequiredStored display identity for the creating actor.
page.updated_actor_kinduser | api_key | systemRequiredPrincipal kind that last changed the page.
page.updated_actor_user_idUUIDOptionalLast updating user ID when actor_kind is user.
page.updated_actor_api_key_idUUIDOptionalLast updating API key ID when actor_kind is api_key.
page.updated_actor_displaystring | nullRequiredStored display identity for the last updating actor.
page.created_atISO 8601 stringRequiredCreation timestamp.
page.updated_atISO 8601 stringRequiredLast update timestamp.
audit_event_idUUID | nullRequiredAppend-only audit event ID, or null for a no-op.
source_revisionintegerOptionalSource revision on a restore result.
replayedbooleanRequiredTrue when an exact Idempotency-Key retry replayed this result.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
invalid_json_body400OptionalThe body is not one JSON value, contains unknown fields, or has invalid field types.
request_body_too_large413OptionalThe JSON request exceeds the route's byte limit.
invalid_idempotency_key400OptionalAn API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters.
idempotency_mismatch409OptionalThe same actor already used this Idempotency-Key for different canonical command input.
if_match_required428OptionalIf-Match is required for this page command.
invalid_if_match400OptionalIf-Match is not the exact strong ETag for this page.
revision_conflict412OptionalThe page has changed. The error details include expected_revision, current_revision, and current_etag; the response ETag is current.
invalid_restore_input400Optionalsource_revision is missing or not a canonical positive integer.
wiki_revision_not_found404OptionalThe source revision does not exist for this page.
wiki_slug_conflict409OptionalRestoring the snapshot would collide with another page slug.
wiki_filename_conflict409OptionalRestoring 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/restore

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

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/templates

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.

Response body

FieldType / statusRequiredDescription
templatesWikiTemplate[]RequiredSystem 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

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe 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/templates

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

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages/{pageId}/export.md

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Response headers

FieldType / statusRequiredDescription
ETagstrong ETagRequiredCurrent page version in the form "wiki-page:{pageId}:rev:{revision}". A revision_conflict response also returns the current value.
Content-DispositionstringRequiredattachment with safe filename and RFC 5987 filename* parameters.
Content-LengthintegerRequiredExact Markdown byte count.
X-Content-Type-OptionsnosniffRequiredPrevents browser MIME sniffing.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe 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.md

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

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages/{pageId}/attachments

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Response body

FieldType / statusRequiredDescription
attachmentsWikiAttachment[]RequiredReady attachment metadata in storage query order.
attachments[].idUUIDRequiredAttachment UUID.
attachments[].project_idUUIDRequiredOwning project UUID.
attachments[].page_idUUIDRequiredOwning wiki page UUID.
attachments[].original_filenamestringRequiredSanitized image filename, at most 180 bytes.
attachments[].content_typeimage/png | image/jpeg | image/webpRequiredVerified media type detected from the bytes.
attachments[].size_bytesintegerRequiredImage size from 1 byte through 8 MiB.
attachments[].sha256stringRequiredLowercase SHA-256 hex digest.
attachments[].widthintegerRequiredVerified width from 1 through 8000 pixels.
attachments[].heightintegerRequiredVerified height from 1 through 8000 pixels.
attachments[].actor_kinduser | api_key | systemRequiredUploading principal kind.
attachments[].actor_user_idUUID | nullRequiredUploading user ID, when applicable.
attachments[].actor_api_key_idUUID | nullRequiredUploading API key ID, when applicable.
attachments[].actor_displaystring | nullRequiredStored display identity for the uploader.
attachments[].ready_atISO 8601 stringRequiredTimestamp when storage and metadata became ready.
attachments[].created_atISO 8601 stringRequiredCreation timestamp.
attachments[].markdownstringRequiredCanonical ![alt](wiki-attachment:{attachmentId}) reference.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe 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/attachments

Response 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": "![architecture](wiki-attachment:d2600000-0000-0000-0000-000000000401)"
    }
  ]
}
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.

Authentication: dual · Scopes: wiki:write

# POST /api/projects/{projectId}/wiki/pages/{pageId}/attachments

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.

Request body

FieldType / statusRequiredDescription
filePNG | JPEG | WebP binaryRequiredThe 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

FieldType / statusRequiredDescription
attachment.idUUIDRequiredAttachment UUID.
attachment.project_idUUIDRequiredOwning project UUID.
attachment.page_idUUIDRequiredOwning wiki page UUID.
attachment.original_filenamestringRequiredSanitized image filename, at most 180 bytes.
attachment.content_typeimage/png | image/jpeg | image/webpRequiredVerified media type detected from the bytes.
attachment.size_bytesintegerRequiredImage size from 1 byte through 8 MiB.
attachment.sha256stringRequiredLowercase SHA-256 hex digest.
attachment.widthintegerRequiredVerified width from 1 through 8000 pixels.
attachment.heightintegerRequiredVerified height from 1 through 8000 pixels.
attachment.actor_kinduser | api_key | systemRequiredUploading principal kind.
attachment.actor_user_idUUID | nullRequiredUploading user ID, when applicable.
attachment.actor_api_key_idUUID | nullRequiredUploading API key ID, when applicable.
attachment.actor_displaystring | nullRequiredStored display identity for the uploader.
attachment.ready_atISO 8601 stringRequiredTimestamp when storage and metadata became ready.
attachment.created_atISO 8601 stringRequiredCreation timestamp.
attachment.markdownstringRequiredCanonical ![alt](wiki-attachment:{attachmentId}) reference.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
invalid_wiki_attachment_multipart400/415OptionalThe request is not multipart/form-data or does not contain exactly one file part.
invalid_wiki_attachment400OptionalThe image part is empty.
invalid_wiki_attachment_type415OptionalThe file part has no valid declared Content-Type.
unsupported_wiki_attachment_type415OptionalDetected bytes are not PNG, JPEG, or WebP.
wiki_attachment_type_mismatch415OptionalDeclared image media type does not match detected bytes.
wiki_attachment_filename_type_mismatch400OptionalFilename extension does not match the image type.
invalid_wiki_attachment_image400OptionalImage bytes are malformed or truncated.
wiki_attachment_dimensions_exceeded400OptionalImage width or height is outside 1-8000 pixels, or total pixels exceed 16 megapixels.
wiki_attachment_request_too_large413OptionalThe multipart request exceeds 10 MiB.
wiki_attachment_too_large413OptionalThe image exceeds 8 MiB.
wiki_page_archived409OptionalArchived pages cannot receive attachments.
wiki_attachment_project_quota_exceeded409OptionalThe project already has 200 images or 50 MiB of wiki image metadata.
wiki_attachment_storage_unavailable503OptionalPrivate object storage could not accept the image.
wiki_attachment_metadata_reconciliation_pending503OptionalThe 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/attachments

Response 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": "![architecture](wiki-attachment:d2600000-0000-0000-0000-000000000401)"
  }
}
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.

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/pages/{pageId}/attachments/{attachmentId}

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.
attachmentIdUUIDRequiredCanonical UUID of an attachment on the page.

Response body

FieldType / statusRequiredDescription
attachment.idUUIDRequiredAttachment UUID.
attachment.project_idUUIDRequiredOwning project UUID.
attachment.page_idUUIDRequiredOwning wiki page UUID.
attachment.original_filenamestringRequiredSanitized image filename, at most 180 bytes.
attachment.content_typeimage/png | image/jpeg | image/webpRequiredVerified media type detected from the bytes.
attachment.size_bytesintegerRequiredImage size from 1 byte through 8 MiB.
attachment.sha256stringRequiredLowercase SHA-256 hex digest.
attachment.widthintegerRequiredVerified width from 1 through 8000 pixels.
attachment.heightintegerRequiredVerified height from 1 through 8000 pixels.
attachment.actor_kinduser | api_key | systemRequiredUploading principal kind.
attachment.actor_user_idUUID | nullRequiredUploading user ID, when applicable.
attachment.actor_api_key_idUUID | nullRequiredUploading API key ID, when applicable.
attachment.actor_displaystring | nullRequiredStored display identity for the uploader.
attachment.ready_atISO 8601 stringRequiredTimestamp when storage and metadata became ready.
attachment.created_atISO 8601 stringRequiredCreation timestamp.
attachment.markdownstringRequiredCanonical ![alt](wiki-attachment:{attachmentId}) reference.
download_urlURLRequiredShort-lived signed private-storage URL.
expires_atISO 8601 stringRequiredApproximate UTC expiry timestamp.
expires_in_secondsintegerRequiredSigned URL lifetime; always 300.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
wiki_attachment_not_found404OptionalThe attachment ID is invalid or does not belong to this page.
wiki_attachment_unavailable503OptionalThe 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-000000000401

Response 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": "![architecture](wiki-attachment:d2600000-0000-0000-0000-000000000401)"
  },
  "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.

Authentication: jwt_only · Scopes: —

# DELETE /api/projects/{projectId}/wiki/pages/{pageId}/attachments/{attachmentId}

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.
pageIdUUIDRequiredCanonical UUID of a wiki page in the project.
attachmentIdUUIDRequiredCanonical UUID of an attachment on the page.

Response body

FieldType / statusRequiredDescription
deletedbooleanRequiredTrue after object and metadata deletion.
attachment.idUUIDRequiredAttachment UUID.
attachment.project_idUUIDRequiredOwning project UUID.
attachment.page_idUUIDRequiredOwning wiki page UUID.
attachment.original_filenamestringRequiredSanitized image filename, at most 180 bytes.
attachment.content_typeimage/png | image/jpeg | image/webpRequiredVerified media type detected from the bytes.
attachment.size_bytesintegerRequiredImage size from 1 byte through 8 MiB.
attachment.sha256stringRequiredLowercase SHA-256 hex digest.
attachment.widthintegerRequiredVerified width from 1 through 8000 pixels.
attachment.heightintegerRequiredVerified height from 1 through 8000 pixels.
attachment.actor_kinduser | api_key | systemRequiredUploading principal kind.
attachment.actor_user_idUUID | nullRequiredUploading user ID, when applicable.
attachment.actor_api_key_idUUID | nullRequiredUploading API key ID, when applicable.
attachment.actor_displaystring | nullRequiredStored display identity for the uploader.
attachment.ready_atISO 8601 stringRequiredTimestamp when storage and metadata became ready.
attachment.created_atISO 8601 stringRequiredCreation timestamp.
attachment.markdownstringRequiredCanonical ![alt](wiki-attachment:{attachmentId}) reference.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_admin_required403OptionalThe operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it.
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_page_not_found404OptionalThe page ID is invalid or no matching page exists in this project.
wiki_attachment_not_found404OptionalThe attachment ID is invalid or does not belong to this page.
wiki_attachment_unavailable503OptionalThe attachment operation is temporarily unavailable.
wiki_attachment_in_use409OptionalCurrent or immutable historical page content still references the attachment.
wiki_attachment_delete_metadata_failed503OptionalThe database could not delete attachment metadata; storage is untouched.
wiki_attachment_delete_reconciliation_pending503OptionalThe metadata-delete response was ambiguous. Storage is untouched and any committed deletion remains durably queued.
wiki_attachment_delete_cleanup_pending503OptionalMetadata 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-000000000401

Response 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": "![architecture](wiki-attachment:d2600000-0000-0000-0000-000000000401)"
  }
}
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.

Authentication: dual · Scopes: wiki:read

# GET /api/projects/{projectId}/wiki/export.zip

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.

Response headers

FieldType / statusRequiredDescription
Content-DispositionstringRequiredattachment filename project-{projectId}-wiki.zip.
Content-LengthintegerRequiredExact completed ZIP byte count.
X-Content-Type-OptionsnosniffRequiredPrevents browser MIME sniffing.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_archive_expanded_too_large413OptionalAn entry or the complete ZIP exceeds 50 MiB expanded.
wiki_archive_page_limit413OptionalThe archive or project contains more than 200 pages.
wiki_archive_attachment_limit413OptionalThe archive or project contains more than 200 attachments.
wiki_archive_output_too_large413OptionalThe generated ZIP exceeds the 25 MiB round-trip limit.
wiki_archive_attachment_corrupt409OptionalStored attachment bytes do not match ready metadata.
wiki_archive_dangling_page_reference409OptionalAn active page contains a stable link to a page excluded from the archive.
wiki_archive_storage_unavailable503OptionalAn 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.zip

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

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/wiki/import

Path parameters

FieldType / statusRequiredDescription
projectIdUUIDRequiredCanonical UUID of the project that owns the independent wiki.

Request headers

FieldType / statusRequiredDescription
Idempotency-KeystringOptionalRequired 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

FieldType / statusRequiredDescription
fileapplication/zip binaryRequiredThe 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

FieldType / statusRequiredDescription
outcomeappliedRequiredSuccessful import outcome.
codewiki_archive_importedRequiredStable success code.
project_idUUIDRequiredDestination project UUID.
archive_sha256stringRequiredSHA-256 of the uploaded ZIP bytes.
page_countintegerRequiredNumber of created pages.
attachment_countintegerRequiredNumber of created attachments.
pagesWikiArchiveImportPage[]RequiredMappings with source_id, destination id, title, slug, filename, source_revision, revision (1), and audit_event_id.
attachmentsWikiArchiveImportAttachment[]RequiredMappings with source_id, destination id, destination page_id, and private object path.
replayedbooleanRequiredTrue for an exact saved retry.

Error codes

FieldType / statusRequiredDescription
project_not_found404OptionalThe project ID is invalid or the project does not exist.
project_access_denied403OptionalThe JWT user cannot access the project or the API key is bound to another project.
authentication_required401OptionalThe authenticated JWT user could not be identified.
wiki_admin_required403OptionalThe operation requires a JWT user who is a workspace owner/admin or explicit project admin. API keys cannot perform it.
invalid_idempotency_key400OptionalAn API-key command omitted Idempotency-Key or the trimmed value exceeds 255 characters.
idempotency_mismatch409OptionalThe same actor already used this Idempotency-Key for different canonical command input.
invalid_wiki_archive400OptionalThe ZIP is malformed, truncated, or fails CRC/length validation.
invalid_wiki_archive_manifest400Optionalwiki-manifest.json is not the exact valid version-1 schema.
wiki_archive_manifest_missing400Optionalwiki-manifest.json is absent from the ZIP root.
unsupported_wiki_archive_version400OptionalThe manifest version is not 1.
unsafe_wiki_archive_path400OptionalAn entry path is absolute, non-canonical, non-ASCII, or traverses directories.
duplicate_wiki_archive_path400OptionalTwo ZIP paths normalize to the same case-insensitive path.
unsupported_wiki_archive_entry400OptionalAn entry is encrypted, not a regular file, or uses unsupported compression.
nested_wiki_archive400OptionalThe ZIP contains another archive file.
invalid_wiki_archive_page400OptionalA page manifest entry or its canonical Markdown metadata is invalid.
wiki_archive_page_missing400OptionalA manifest page path is absent from the ZIP.
invalid_wiki_archive_attachment400OptionalAn attachment manifest entry, filename, media type, or metadata is invalid.
wiki_archive_attachment_missing400OptionalA manifest attachment path is absent from the ZIP.
wiki_archive_attachment_size_mismatch400OptionalAttachment bytes do not match the manifest size.
wiki_archive_attachment_hash_mismatch400OptionalAttachment bytes do not match the manifest SHA-256.
invalid_wiki_archive_markdown400OptionalA Markdown entry is not valid UTF-8 or contains NUL.
wiki_archive_page_collision409OptionalImported page IDs, slugs, filenames, or paths collide.
wiki_archive_id_collision409OptionalManifest page and attachment IDs collide.
wiki_archive_attachment_collision409OptionalManifest attachment paths collide.
wiki_archive_compressed_too_large413OptionalThe ZIP exceeds 25 MiB compressed.
wiki_archive_expanded_too_large413OptionalAn entry or the complete ZIP exceeds 50 MiB expanded.
wiki_archive_compression_ratio413OptionalAn entry or the complete ZIP exceeds a 100:1 expansion ratio.
wiki_archive_entry_limit413OptionalThe ZIP does not contain 1-500 regular file entries.
wiki_archive_page_limit413OptionalThe archive or project contains more than 200 pages.
wiki_archive_attachment_limit413OptionalThe archive or project contains more than 200 attachments.
wiki_archive_markdown_too_large413OptionalOne imported Markdown page exceeds 1 MiB.
wiki_archive_output_too_large413OptionalThe generated ZIP exceeds the 25 MiB round-trip limit.
invalid_wiki_archive_multipart400/415OptionalThe request is not the exact one-file application/zip multipart contract.
unexpected_wiki_archive_entry400OptionalThe ZIP has a file not referenced by the manifest.
wiki_attachment_project_quota_exceeded409OptionalImport would exceed the destination project's 200-image or 50 MiB wiki quota.
wiki_archive_storage_unavailable503OptionalAttachment staging failed and completed cleanup succeeded.
wiki_archive_staging_cleanup_pending503OptionalAttachment staging failed and storage cleanup remains pending.
wiki_archive_database_cleanup_pending503OptionalPublication failed and staged-object cleanup remains pending.
wiki_archive_replay_cleanup_pending503OptionalReplay 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/import

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

Authentication: dual · Scopes: —

# POST /api/projects/{projectId}/app-control/requests

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredAPI key's project UUID.

Request headers

FieldType / statusRequiredDescription
Idempotency-KeystringRequiredStable logical-request identity. Reuse only for an exact retry.

Request body

FieldType / statusRequiredDescription
operationstringRequiredAllowlisted high-impact app-control operation.
callobjectRequiredExact typed params, query, body, and mutation preconditions for the operation.

Response body

FieldType / statusRequiredDescription
request_idstringRequiredDurable approval request UUID.
project_idstringRequiredProject derived from the API key or stored request.
operationstringRequiredHigh-impact operation awaiting or using approval.
safe_summaryobjectRequiredServer-derived operation and resource IDs; never contains the request body.
statusstringRequiredpending, approved, denied, executing, completed, failed, or expired.
expires_atstringRequiredApproval expiry timestamp.
approval_pathstringRequiredStatic browser route where a project administrator reviews the action.

Error codes

FieldType / statusRequiredDescription
app_control_approval_not_required400OptionalThe operation is routine and must use its normal endpoint.
app_control_insufficient_scope403OptionalThe key lacks the operation's existing scope.
app_control_idempotency_mismatch409OptionalThe 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/requests

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

Authentication: dual · Scopes: —

# GET /api/app-control/requests/{requestId}

Path parameters

FieldType / statusRequiredDescription
requestIdstringRequiredApproval request UUID.

Response body

FieldType / statusRequiredDescription
request_idstringRequiredDurable approval request UUID.
project_idstringRequiredProject derived from the API key or stored request.
operationstringRequiredHigh-impact operation awaiting or using approval.
safe_summaryobjectRequiredServer-derived operation and resource IDs; never contains the request body.
statusstringRequiredpending, approved, denied, executing, completed, failed, or expired.
expires_atstringRequiredApproval expiry timestamp.
approval_pathstringRequiredStatic 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_UUID

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

Authentication: jwt_only · Scopes: —

# POST /api/app-control/requests/{requestId}/decision

Path parameters

FieldType / statusRequiredDescription
requestIdstringRequiredApproval request UUID.

Request headers

FieldType / statusRequiredDescription
Idempotency-KeystringRequiredStable logical-request identity. Reuse only for an exact retry.

Request body

FieldType / statusRequiredDescription
approvedbooleanRequiredTrue to approve, false to deny.

Response body

FieldType / statusRequiredDescription
request_idstringRequiredDurable approval request UUID.
project_idstringRequiredProject derived from the API key or stored request.
operationstringRequiredHigh-impact operation awaiting or using approval.
safe_summaryobjectRequiredServer-derived operation and resource IDs; never contains the request body.
statusstringRequiredpending, approved, denied, executing, completed, failed, or expired.
expires_atstringRequiredApproval expiry timestamp.
approval_pathstringRequiredStatic browser route where a project administrator reviews the action.

Error codes

FieldType / statusRequiredDescription
app_control_admin_required403OptionalThe 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/decision

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

Authentication: dual · Scopes: —

# POST /api/app-control/requests/{requestId}/execute

Path parameters

FieldType / statusRequiredDescription
requestIdstringRequiredApproved request UUID.

Request body

FieldType / statusRequiredDescription
operationstringRequiredAllowlisted high-impact app-control operation.
callobjectRequiredExact typed params, query, body, and mutation preconditions for the operation.

Response body

FieldType / statusRequiredDescription
resultobjectRequiredCanonical result from the underlying domain command.

Error codes

FieldType / statusRequiredDescription
app_control_approval_pending409OptionalThe request still awaits a human decision.
app_control_request_denied403OptionalThe request was denied.
app_control_request_expired410OptionalThe request expired.
app_control_request_not_found404OptionalKey, 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/execute

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

Authentication: dual · Scopes: triggers:read

# GET /api/projects/{projectId}/triggers

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
idstringRequiredTrigger UUID.
namestringRequiredTrigger display name.
job_idstringRequiredUUID of the associated job.
cron_expressionstringRequiredCron schedule expression.
enabledbooleanRequiredWhether 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/triggers

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

Authentication: dual · Scopes: triggers:write

# POST /api/projects/{projectId}/triggers

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
namestringRequiredTrigger display name.
job_idstringRequiredUUID of the job to execute.
cron_expressionstringRequiredCron schedule expression (5-field).
enabledbooleanOptionalWhether the trigger is active. Defaults to true.

Response body

FieldType / statusRequiredDescription
idstringRequiredUUID of the newly created trigger.
namestringRequiredTrigger name.
cron_expressionstringRequiredCron schedule.
enabledbooleanRequiredActive state.
created_atstringRequiredISO 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/triggers

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

Authentication: dual · Scopes: triggers:read

# GET /api/triggers/{triggerId}

Path parameters

FieldType / statusRequiredDescription
triggerIdstringRequiredUUID of the trigger.

Response body

FieldType / statusRequiredDescription
idstringRequiredTrigger UUID.
namestringRequiredTrigger name.
job_idstringRequiredAssociated job UUID.
cron_expressionstringRequiredCron schedule expression.
enabledbooleanRequiredWhether the trigger is active.
created_atstringRequiredISO 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-001

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

Authentication: dual · Scopes: triggers:write

# PUT /api/triggers/{triggerId}

Path parameters

FieldType / statusRequiredDescription
triggerIdstringRequiredUUID of the trigger.

Request body

FieldType / statusRequiredDescription
namestringOptionalNew trigger name.
job_idstringOptionalNew job UUID.
cron_expressionstringOptionalNew cron schedule expression.
enabledbooleanOptionalEnable or disable the trigger.

Response body

FieldType / statusRequiredDescription
idstringRequiredTrigger UUID.
namestringRequiredUpdated trigger name.
cron_expressionstringRequiredUpdated cron schedule.
enabledbooleanRequiredUpdated active state.
updated_atstringRequiredISO 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-001

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

Authentication: dual · Scopes: triggers:write

# DELETE /api/triggers/{triggerId}

Path parameters

FieldType / statusRequiredDescription
triggerIdstringRequiredUUID of the trigger to delete.

Response body

FieldType / statusRequiredDescription
messagestringRequiredConfirmation 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-001

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

Authentication: dual · Scopes: queues:read

# GET /api/projects/{projectId}/queues

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue UUID.
namestringRequiredQueue display name.
descriptionstringOptionalOptional queue description.
max_retriesnumberRequiredMaximum retry attempts per item.
custom_statusesstring[]RequiredQueue-specific custom statuses.
created_atstringRequiredISO 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/queues

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

Authentication: dual · Scopes: queues:write

# POST /api/projects/{projectId}/queues

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Request body

FieldType / statusRequiredDescription
namestringRequiredQueue display name.
descriptionstringOptionalOptional description.
max_retriesnumberOptionalMaximum retry attempts per item. Defaults to 3.
retry_delay_secondsnumberOptionalDelay between retries in seconds. Defaults to 60.
deadline_secondsnumberOptionalMaximum time in seconds before an in-progress item is considered timed out.
auto_trigger_job_idstringOptionalUUID of a job to auto-trigger when items are added.
custom_statusesstring[]OptionalAdditional queue-specific statuses allowed by the custom status endpoint.

Response body

FieldType / statusRequiredDescription
idstringRequiredUUID of the newly created queue.
namestringRequiredQueue name.
max_retriesnumberRequiredConfigured max retries.
created_atstringRequiredISO 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/queues

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

Authentication: dual · Scopes: queues:read

# GET /api/queues/{queueId}

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue UUID.
namestringRequiredQueue name.
descriptionstringOptionalQueue description.
max_retriesnumberRequiredMaximum retries.
retry_delay_secondsnumberRequiredDelay between retries.
deadline_secondsnumberOptionalItem processing deadline.
custom_statusesstring[]RequiredQueue-specific custom statuses.
created_atstringRequiredISO 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

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

Authentication: dual · Scopes: queues:write

# PUT /api/queues/{queueId}

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Request body

FieldType / statusRequiredDescription
namestringOptionalNew queue name.
descriptionstringOptionalNew description.
max_retriesnumberOptionalNew max retry count.
retry_delay_secondsnumberOptionalNew retry delay.
deadline_secondsnumberOptionalNew processing deadline.
auto_trigger_job_idstringOptionalNew auto-trigger job UUID.
custom_statusesstring[]OptionalReplace the queue-specific custom statuses.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue UUID.
namestringRequiredUpdated queue name.
max_retriesnumberRequiredUpdated max retries.
updated_atstringRequiredISO 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-001

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

Authentication: dual · Scopes: queues:write

# DELETE /api/queues/{queueId}

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue to delete.

Response body

FieldType / statusRequiredDescription
messagestringRequiredConfirmation 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-001

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

Authentication: dual · Scopes: queues:read

# GET /api/queues/{queueId}/stats

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Response body

FieldType / statusRequiredDescription
totalnumberRequiredTotal items in the queue.
status_countsobjectRequiredCounts keyed by item status.
avg_processing_msnumberRequiredAverage processing duration for completed items.
due_todaynumberRequiredNew items due by the end of today.
retry_ratenumberRequiredPercentage 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/stats

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

Authentication: dual · Scopes: queues:read

# GET /api/queues/{queueId}/items

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Query parameters

FieldType / statusRequiredDescription
statusstringOptionalFilter by status: "new", "in_progress", "completed", "failed", "abandoned".
referencestringOptionalFilter by reference identifier.
limitnumberOptionalMaximum number of items to return. (Default: 50)
offsetnumberOptionalNumber of items to skip for pagination. (Default: 0)

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredItem status.
referencestringOptionalExternal reference identifier.
prioritynumberRequiredItem priority (higher = more urgent).
created_atstringRequiredISO 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.

Authentication: dual · Scopes: queues:write

# POST /api/queues/{queueId}/items

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Request body

FieldType / statusRequiredDescription
dataobjectRequiredArbitrary JSON payload for the item.
referencestringOptionalExternal reference identifier (e.g. invoice number).
prioritynumberOptionalItem priority. Higher values are processed first.
tagsstring[]OptionalTags for categorization and filtering.
due_datestringOptionalISO 8601 deadline for processing this item.
defer_untilstringOptionalISO 8601 timestamp before which the item should not be processed.

Response body

FieldType / statusRequiredDescription
idstringRequiredUUID of the newly created item.
statusstringRequiredInitial status ("new").
referencestringOptionalReference identifier.
created_atstringRequiredISO 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/items

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

Authentication: dual · Scopes: queues:write

# POST /api/queues/{queueId}/items/bulk

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Request body

FieldType / statusRequiredDescription
itemsQueueItemInput[]RequiredArray of items to add. Each item has: data (required), reference, priority, tags, due_date, defer_until.

Response body

FieldType / statusRequiredDescription
creatednumberRequiredNumber of items successfully created.
itemsQueueItem[]RequiredArray 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/bulk

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

Authentication: dual · Scopes: queues:execute

# POST /api/queues/{queueId}/next

Path parameters

FieldType / statusRequiredDescription
queueIdstringRequiredUUID of the queue.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredUpdated status ("in_progress").
dataobjectRequiredItem payload.
referencestringOptionalExternal reference.
retry_countnumberRequiredNumber 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/next

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

Authentication: dual · Scopes: queues:read

# GET /api/queue-items/{itemId}

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
queue_idstringRequiredUUID of the parent queue.
statusstringRequiredCurrent item status.
dataobjectRequiredItem payload.
referencestringOptionalExternal reference.
retry_countnumberRequiredNumber of retries attempted.
created_atstringRequiredISO 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-001

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

Authentication: dual · Scopes: queues:write

# PUT /api/queue-items/{itemId}

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Request body

FieldType / statusRequiredDescription
dataobjectOptionalUpdated item payload.
referencestringOptionalUpdated reference identifier.
prioritynumberOptionalUpdated priority.
tagsstring[]OptionalUpdated tags.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredCurrent item status.
updated_atstringRequiredISO 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-001

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

Authentication: dual · Scopes: queues:write

# POST /api/queue-items/{itemId}/status

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Request body

FieldType / statusRequiredDescription
statusstringRequiredDefault or queue-defined custom status.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredUpdated 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/status

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

Authentication: dual · Scopes: queues:write

# DELETE /api/queue-items/{itemId}

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item to delete.

Response body

FieldType / statusRequiredDescription
messagestringRequiredConfirmation 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-001

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

Authentication: dual · Scopes: queues:execute

# POST /api/queue-items/{itemId}/complete

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Request body

FieldType / statusRequiredDescription
outputobjectOptionalOptional output data from processing (results, generated files, etc.).

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredUpdated status ("completed").
completed_atstringRequiredISO 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/complete

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

Authentication: dual · Scopes: queues:execute

# POST /api/queue-items/{itemId}/fail

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Request body

FieldType / statusRequiredDescription
error_messagestringOptionalError message describing the failure.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredUpdated status ("failed").
retry_countnumberRequiredCurrent retry count.
max_retriesnumberRequiredMax 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/fail

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

Authentication: dual · Scopes: queues:execute

# POST /api/queue-items/{itemId}/abandon

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredUpdated 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/abandon

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

Authentication: dual · Scopes: queues:execute

# POST /api/queue-items/{itemId}/retry

Path parameters

FieldType / statusRequiredDescription
itemIdstringRequiredUUID of the queue item.

Response body

FieldType / statusRequiredDescription
idstringRequiredQueue item UUID.
statusstringRequiredUpdated status ("new").
retry_countnumberRequiredUpdated 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/retry

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

Authentication: jwt_only · Scopes: —

# POST /api/projects/{projectId}/api-keys

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project that will own the key.

Request body

FieldType / statusRequiredDescription
namestringRequiredDisplay name for the API key.
scopesstring[]RequiredArray of granted scopes such as "jobs:read", "runs:write", or "queues:execute".
expires_atstringOptionalISO 8601 expiration timestamp. Omit for a non-expiring key.

Response body

FieldType / statusRequiredDescription
idstringRequiredAPI key UUID.
keystringRequiredThe raw API key value (bo_live_...). Shown only once.
namestringRequiredKey display name.
scopesstring[]RequiredGranted scopes.
created_atstringRequiredISO 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-keys

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

Authentication: jwt_only · Scopes: —

# GET /api/projects/{projectId}/api-keys

Path parameters

FieldType / statusRequiredDescription
projectIdstringRequiredUUID of the project.

Response body

FieldType / statusRequiredDescription
idstringRequiredAPI key UUID.
namestringRequiredKey display name.
key_prefixstringRequiredFirst 12 characters of the key for identification.
scopesstring[]RequiredGranted scopes.
last_used_atstringOptionalISO 8601 timestamp of last use.
created_atstringRequiredISO 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-keys

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

Authentication: jwt_only · Scopes: —

# DELETE /api/api-keys/{keyId}

Path parameters

FieldType / statusRequiredDescription
keyIdstringRequiredUUID of the API key to revoke.

Response body

FieldType / statusRequiredDescription
messagestringRequiredConfirmation 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-001

Response example

{
  "message": "API key revoked"
}