Reference · v1

Bracework’s REST API, one page.

The four endpoints a paying team’s integration engineer needs: GET /api/v1/repos lists every repo Bracework is connected to for your tenant, GET /api/v1/findingslists each dependent package we’ve triaged — paginated, filterable by severity and CI lifecycle status, GET /api/v1/findings/[id] returns the full detail record for a single finding by id, and GET /api/v1/prslists every Bracework-authored fix PR — paginated, filterable by repoId and PR status. Cookie-gated, scope-strict, and contract-validated before the response ships.

Authentication

Cookie-gated — the same better-auth session as the rest of /api.

Every /api/v1 endpoint accepts the better-auth session cookie set after /login or /signup. Send it as `Cookie: <name>=<value>`. Missing / expired / invalid cookies collapse to 401 {"error":"Unauthorized"}. Bracework v1 does NOT issue bearer tokens or API keys yet — when that changes, this page and `CHANGELOG` will document it before the field goes live.

Base URL

$HOST + the route below.

Each curl example uses `$HOST` as a placeholder. Set it in your shell before running: `export HOST="https://bracework.example.com"` for production, `export HOST="http://localhost:3000"` for local dev.

Cookies / headers

Only Cookie: is read.

Endpoints do not read Authorization headers today. The session cookie is set automatically by /login and /signup; copy its Cookie:value from your browser’s dev tools before running curl.

GET/api/v1/reposEndpoint 1 · listConnectedRepos

List every GitHub repo Bracework is connected to for the signed-in tenant, ordered by most-recent scan.

Authentication

Cookie gate — send the better-auth session.

Send the better-auth session cookie set after /login or /signup as `Cookie:`. Endpoints return 401 {"error":"Unauthorized"} when the cookie is missing, expired, or invalid. Bracework v1 ships with cookie auth today — there is no bearer-token issuance, no API key surface, and no separate token endpoint.

Worked examples

Worked curl and response examples for /api/v1/repos

shellRequest
# Set HOST to your Bracework deployment before running curl.
# Production:  export HOST="https://bracework.example.com"
# Local dev:   export HOST="http://localhost:3000"
# Session cookie: copy from the browser dev tools after logging in at /login.
#   export COOKIE="better-auth.session_token=…"
curl -b "$COOKIE" "$HOST/api/v1/repos"
json200 response
[
  {
    "id": "cm0repo000000000000000000r1",
    "owner": "acme",
    "repo": "core-api",
    "defaultBranch": "main",
    "isDemoRow": false,
    "lastScan": {
      "id": "cm0scan000000000000000000s1",
      "startedAt": "2026-08-15T03:00:04.221Z",
      "finishedAt": "2026-08-15T03:02:51.117Z",
      "status": "succeeded",
      "findingsSeen": 7,
      "draftsOpened": 2,
      "error": null
    },
    "scanStatusBadge": "green",
    "openPrCount": 1,
    "status": "triage_in_progress",
    "openAdvisoryCount": 5,
    "lastScanAt": "2026-08-15T03:00:04.221Z",
    "lastScanRelative": "yesterday",
    "hasOpenFixPr": true,
    "openFixDraftId": "cm0find000000000000000000f9",
    "ownerInitial": "ac"
  }
]
json401 response (no / expired cookie)
{
  "error": "Unauthorized"
}
GET/api/v1/findingsEndpoint 2 · listFindings

List each dependent package Bracework has triaged for the signed-in tenant — paginated, optionally filtered by repoId, severity, and CI lifecycle status.

Authentication

Cookie gate — send the better-auth session.

Send the better-auth session cookie set after /login or /signup as `Cookie:`. Endpoints return 401 {"error":"Unauthorized"} when the cookie is missing, expired, or invalid. Bracework v1 ships with cookie auth today — there is no bearer-token issuance, no API key surface, and no separate token endpoint.

Query parameters

Query parameters for /api/v1/findings

NameTypeRequiredDescription
pageintegerOptional1-indexed page number; combined with `limit`.default: 1
limitintegerOptionalPage size; maximum 100.default: 25
repoIdstringOptionalExact match on a ConnectedRepo id from `GET /api/v1/repos`. Out-of-scope (other-user) repoIds collapse to an empty result, not 404.
severitystringOptionalOne of `critical`, `high`, `medium`, `low`. Filters the current page only — see the note below.
statusstringOptionalOne of `pending`, `drafting`, `escalated`, `resolved`. Filters the current page only — see the note below.
Implementation notes

Implementation notes for /api/v1/findings

  • The `severity` and `status` query parameters narrow the items on the current page only. `totalItems` and `totalPages` reflect the unscoped count for your `userId` (plus an optional `repoId`); when filtering, `items.length` can be smaller than `limit × totalPages`. To fetch every matching finding, page through with no severity/status filter.
Worked examples

Worked curl and response examples for /api/v1/findings

shellRequest (filtered)
# Set HOST to your Bracework deployment before running curl.
# Production:  export HOST="https://bracework.example.com"
# Local dev:   export HOST="http://localhost:3000"
# Session cookie: copy from the browser dev tools after logging in at /login.
#   export COOKIE="better-auth.session_token=…"
curl -b "$COOKIE" "$HOST/api/v1/findings?limit=50&severity=high&status=drafting"
json200 response
{
  "items": [
    {
      "id": "cm0find000000000000000000f1",
      "repoId": "cm0repo000000000000000000r1",
      "advisoryId": "GHSA-abcd-1234-efgh",
      "package": "lodash",
      "ecosystem": "npm",
      "vulnerableRange": "<4.17.21",
      "fixedRange": ">=4.17.21",
      "severity": "high",
      "reachability": 0.9,
      "exploitSignal": 0.5,
      "status": "drafting",
      "ciStatus": "success",
      "discoveredAt": "2026-08-15T03:01:09.000Z",
      "updatedAt": "2026-08-15T03:02:33.000Z",
      "reconciledAt": null
    }
  ],
  "page": 1,
  "limit": 25,
  "totalItems": 12,
  "totalPages": 1
}
json401 response (no / expired cookie)
{
  "error": "Unauthorized"
}
GET/api/v1/findings/[id]Endpoint 3 · getFinding

Fetch one triaged finding by id — full detail record with advisory id, package, ecosystem, vulnerable / fixed range, severity, reachability, exploit signal, lifecycle status, and timestamps.

Authentication

Cookie gate — send the better-auth session.

Send the better-auth session cookie set after /login or /signup as `Cookie:`. Endpoints return 401 {"error":"Unauthorized"} when the cookie is missing, expired, or invalid. Bracework v1 ships with cookie auth today — there is no bearer-token issuance, no API key surface, and no separate token endpoint.

Implementation notes

Implementation notes for /api/v1/findings/[id]

  • The path id is the FixDraft row id surfaced by `GET /api/v1/findings` (the `id` of each `V1Finding`). Out-of-scope (other-user) ids and missing ids collapse to the same 404 {"error":"finding not found"} body so the caller cannot distinguish "not yours" from "not real" (this endpoint's 404 body is intentionally distinct from the project-wide `"Not found"` literal used by the list endpoint).
  • Programmatic callers may also authenticate with `Authorization: Bearer <token>` using a token minted from `/settings/api-tokens` — the route accepts EITHER the better-auth session cookie OR a Bearer token (resolved against the apiToken table) and resolves both to the same user scope.
Worked examples

Worked curl and response examples for /api/v1/findings/[id]

shellRequest
# Set HOST to your Bracework deployment before running curl.
# Production:  export HOST="https://bracework.example.com"
# Local dev:   export HOST="http://localhost:3000"
# Session cookie: copy from the browser dev tools after logging in at /login.
#   export COOKIE="better-auth.session_token=…"
curl -b "$COOKIE" "$HOST/api/v1/findings/$FINDING_ID"
json200 response
{
  "id": "cm0find000000000000000000f1",
  "repoId": "cm0repo000000000000000000r1",
  "advisoryId": "GHSA-abcd-1234-efgh",
  "package": "lodash",
  "ecosystem": "npm",
  "vulnerableRange": "<4.17.21",
  "fixedRange": ">=4.17.21",
  "severity": "high",
  "reachability": 0.9,
  "exploitSignal": 0.5,
  "status": "drafting",
  "ciStatus": "success",
  "discoveredAt": "2026-08-15T03:01:09.000Z",
  "updatedAt": "2026-08-15T03:02:33.000Z",
  "reconciledAt": null,
  "prCount": 1
}
json401 response (no / expired cookie)
{
  "error": "Unauthorized"
}
json404 response (missing or out-of-scope id)
{
  "error": "finding not found"
}
GET/api/v1/prsEndpoint 4 · listPrs

List every Bracework-authored fix PR for the signed-in tenant — paginated, optionally filtered by repoId and PR status — with each item carrying repo (owner/repo), advisory id, package, vulnerable vs fixed version, branch name, codemod summary, CI status, and timestamps.

Authentication

Cookie gate — send the better-auth session.

Composite gate — accepts EITHER the better-auth session cookie (`Cookie:`) OR `Authorization: Bearer <token>` (a token minted from `/settings/api-tokens`, resolved against the apiToken table) — the same composite the `GET /api/v1/findings/[id]` route uses. Missing/expired/invalid creds on either path collapse to a single 401 {"error":"Unauthorized"}.

Query parameters

Query parameters for /api/v1/prs

NameTypeRequiredDescription
pageintegerOptional1-indexed page number; combined with `limit`.default: 1
limitintegerOptionalPage size; maximum 100.default: 25
repoIdstringOptionalExact match on a ConnectedRepo id from `GET /api/v1/repos`. Out-of-scope (other-user) repoIds collapse to an empty result, not 404.
statusstringOptionalOne of `open`, `merged`, `closed`. The underlying `prState` column is mapped: `open` → `"open"`, `merged` → `"merged"`, `closed` → `null | "closed" | "cancelled"`. Filtering runs at the SQL layer, so `totalItems` reflects the filtered count.
Implementation notes

Implementation notes for /api/v1/prs

  • The `status` query param maps to the underlying FixDraft.prState column. The stored values are freeform; the v1 surface exposes a three-value enum and maps `null`, `"closed"`, and `"cancelled"` to the same `closed` bucket. The filter is applied in SQL, so `totalItems` and `totalPages` reflect the filtered count (unlike `/api/v1/findings`, where severity/status narrow the current page only).
  • Each item carries a nested `repo: { owner, repo }` so the caller can render the row without a follow-up resolve against `/api/v1/repos`. The `branch` field is the source branch the fix PR was opened from (e.g. `fix/GHSA-…`) — surfaced from `FixDraft.branch` so integration engineers can group PRs by branch without re-fetching `/pr/[id]`.
Worked examples

Worked curl and response examples for /api/v1/prs

shellRequest (filtered)
# Set HOST to your Bracework deployment before running curl.
# Production:  export HOST="https://bracework.example.com"
# Local dev:   export HOST="http://localhost:3000"
# Session cookie: copy from the browser dev tools after logging in at /login.
#   export COOKIE="better-auth.session_token=…"
curl -b "$COOKIE" "$HOST/api/v1/prs?repoId=$REPO_ID&status=open"
json200 response
{
  "items": [
    {
      "id": "cm0fix000000000000000000p1",
      "repoId": "cm0repo000000000000000000r1",
      "repo": { "owner": "acme", "repo": "core-api" },
      "advisoryId": "GHSA-abcd-1234-efgh",
      "package": "lodash",
      "fromVersion": "<4.17.21",
      "toVersion": ">=4.17.21",
      "branch": "fix/GHSA-abcd-1234-efgh",
      "hasCodemod": true,
      "codemodSummary": "const _ = require('lodash');
function bump(o) { return Object.assign({}, o, { __v: 1 }); }…",
      "ciStatus": "success",
      "prUrl": "https://github.com/acme/core-api/pull/42",
      "prState": "open",
      "createdAt": "2026-08-15T03:01:09.000Z",
      "updatedAt": "2026-08-15T03:02:33.000Z",
      "reconciledAt": null
    }
  ],
  "page": 1,
  "limit": 25,
  "totalItems": 3,
  "totalPages": 1
}
json401 response (no session / invalid Bearer)
{
  "error": "Unauthorized"
}
Building on top of Bracework?

Hand this page to your integration engineer — we’ll join when they hit a wall.

The reference above is everything you need to call /api/v1/repos, /api/v1/findings, and /api/v1/findings/[id]today. If their curl returns something unexpected, our integration team can join the Slack thread in under a business day — one engineer, one channel, one PR.