> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testorim.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Get one run with all artifacts

> The polling target named by `pollUrl` in the trigger response.
Returns the complete run row (steps, network and console events,
accessibility violations, performance metrics, visual-diff results,
downloads) plus resolved artifact URLs.

Two encrypted-at-rest columns, `cookiesJson` and `localStorageJson`,
are stripped from the response by `augmentDetail` and never appear
here.

A run is finished when `status` is `completed`, `failed` or
`cancelled`. Use `overallStatus` (`passed` / `failed`) for the
pass/fail verdict. The reference CLI exits 0 only on
`overallStatus === "passed"`.




## OpenAPI

````yaml /api-reference/openapi.yaml get /api/runs/{id}
openapi: 3.1.0
info:
  title: Testorim API
  version: 1.0.0
  summary: AI browser QA. Trigger and inspect test runs from CI or your terminal.
  description: >
    Testorim runs plain-English browser QA tests in a real Playwright browser.

    This document describes the **API-key-authenticated subset** of the HTTP

    API, meaning the endpoints a CI pipeline, a terminal, or a third-party

    integration is expected to call.


    ## Scope and honesty notes


    * **This is not a frozen contract.** `1.0.0` is a documentation version,
      not a server version. The API is **unversioned in the URL**. There is
      no `/v1` prefix; every router mounts flat under `/api`.
    * **This is a subset.** The app's own SPA calls many more endpoints
      (billing, Polar/Clerk/GitHub webhooks, Slack and GitHub OAuth
      callbacks, fixtures, environments, schedules, alerts, visual
      baselines, run sharing, flakiness, test-data variables, perf budgets,
      role probes, bug reports, org/invite management). Those are
      deliberately **not** documented here: they are either session-oriented,
      provider-signed, or not stable enough to hand to an integrator.
      `PATCH`/`DELETE` on projects and procedures also exist in the code but
      are out of scope for this reference.

    ## Authentication


    Send the API key as an **HTTP Bearer token**:


    ```

    Authorization: Bearer tst_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

    ```


    Testorim reads exactly one header, `Authorization`, and accepts either

    credential on it:


    1. a value matching `tst_live_…` (prefix + at least 16 more chars) is
       verified as an API key by SHA-256 hash lookup, or
    2. anything else is verified as a Clerk session JWT (the browser path).


    An API key is bound to **one user and one organization**. On every

    request the middleware pins `currentOrg` to the key's org, so a key

    issued for org A can never read or write org B, even when the owning

    user belongs to both. If the user has since been removed from that org,

    the key stops working (`401`).


    Every list and lookup is org-scoped. A resource belonging to another

    organization returns `404`, never `403`, so the API does not leak the

    existence of rows you cannot see.


    ## Role restrictions


    Members of an org with role `viewer` are read-only: any

    non-`GET`/`HEAD`/`OPTIONS` request is rejected with `403` and

    `code: "read_only_role"`. This applies to API-key callers too, because

    the key inherits the user's role in the bound org.


    ## Rate limiting


    Limits are enforced as a sliding window:


    | Limiter | Policy | Keyed by | Applied to |

    | --- | --- | --- | --- |

    | `api:global` | 600 / minute | client IP | every `/api/*` request |

    | `procedure:minute` | 30 / minute | authenticated user id | `POST
    /api/runs/trigger` |

    | `api:read` | 120 / minute | authenticated user id | `GET /api/runs/active`
    |

    | `auth:strict` | 5 / 15 minutes | client IP | **failed** credential checks
    |


    A throttled request returns `429`, a JSON body of

    `{ "error": …, "retryAfter": <seconds> }`, and a `Retry-After` response

    header. **There are no `X-RateLimit-*` headers**. The middleware sets

    only `Retry-After`, so do not write a client that depends on quota

    headers.


    Rate limits are separate from **plan quotas**. Exhausting the

    organization's monthly run allowance also returns `429`, but from

    `checkRunLimit` rather than the limiter, and with no `Retry-After`.


    ## Request body rules


    * JSON bodies are capped at **1 MB**.

    * Any string anywhere in the body containing a NUL byte (`U+0000`)
      rejects the whole request with `400` before it reaches a handler.
    * Bodies are validated with Zod. A schema failure returns `400` with
      `{ "error": "Invalid request body", "issues": [...] }`, where `issues`
      is the raw `ZodError.issues` array.
  contact:
    name: Testorim support
    email: info@fulgic.com
    url: https://testorim.com
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://app.testorim.com
    description: >-
      Production. Single-origin deployment: Caddy on one GCP VM serves the SPA
      at `/` and reverse-proxies `/api/*` and `/ws` to the backend. This is also
      the default API base URL used by the Testorim CLI.
  - url: http://localhost:3001
    description: >-
      A locally running Testorim backend, for self-hosted or development use.
      The default port is 3001.
security:
  - ApiKeyAuth: []
tags:
  - name: Runs
    description: >-
      Trigger a test run and read its result. `POST /api/runs/trigger` is the
      endpoint CI actually uses.
  - name: Procedures
    description: Saved, replayable test sequences belonging to a project.
  - name: Projects
    description: A website-under-test, scoped to an organization.
  - name: API keys
    description: >-
      Mint, list and revoke API keys. Creating a key requires a browser session.
      See the operation notes.
  - name: Identity
    description: Who the presented credential belongs to.
paths:
  /api/runs/{id}:
    get:
      tags:
        - Runs
      summary: Get one run with all artifacts
      description: |
        The polling target named by `pollUrl` in the trigger response.
        Returns the complete run row (steps, network and console events,
        accessibility violations, performance metrics, visual-diff results,
        downloads) plus resolved artifact URLs.

        Two encrypted-at-rest columns, `cookiesJson` and `localStorageJson`,
        are stripped from the response by `augmentDetail` and never appear
        here.

        A run is finished when `status` is `completed`, `failed` or
        `cancelled`. Use `overallStatus` (`passed` / `failed`) for the
        pass/fail verdict. The reference CLI exits 0 only on
        `overallStatus === "passed"`.
      operationId: getRun
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                  - run
                properties:
                  run:
                    $ref: '#/components/schemas/RunDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >-
            No such run in the key's organization. Note the reference CLI treats
            a 404 in the first seconds after a trigger as benign and keeps
            polling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Run not found
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - ApiKeyAuth: []
components:
  parameters:
    RunId:
      name: id
      in: path
      required: true
      description: >-
        Run id (UUID). Returned as `runId` by `POST /api/runs/trigger`. Not
        UUID-validated by the handler; a malformed value simply finds nothing
        and 404s.
      schema:
        type: string
        format: uuid
  schemas:
    RunDetail:
      type: object
      title: RunDetail
      description: |
        The full run row from `GET /api/runs/{id}`, minus the two encrypted
        columns `cookiesJson` and `localStorageJson` (stripped by
        `augmentDetail`), plus three resolved artifact URLs.

        The JSON payload columns are deliberately typed loosely below: they
        are written by the run orchestrator without a wire schema, so their
        inner shape can change without an API change. Read them defensively.
      required:
        - id
        - projectId
        - instruction
        - status
        - passedCount
        - failedCount
        - skippedCount
        - totalDurationMs
        - llmTokensUsed
        - llmCostCents
        - browserMinutesUsed
        - createdAt
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        procedureId:
          type:
            - string
            - 'null'
          format: uuid
        triggeredByUserId:
          type:
            - string
            - 'null'
          format: uuid
          description: >-
            The user who triggered the run. For an API-key call, the key's
            owner. Nulled if that user is deleted.
        instruction:
          type: string
        userExpectation:
          oneOf:
            - $ref: '#/components/schemas/UserExpectation'
            - type: 'null'
        status:
          $ref: '#/components/schemas/RunStatus'
        overallStatus:
          oneOf:
            - $ref: '#/components/schemas/OverallStatus'
            - type: 'null'
        passedCount:
          type: integer
        failedCount:
          type: integer
        skippedCount:
          type: integer
        totalDurationMs:
          type: integer
        initialUrl:
          type:
            - string
            - 'null'
        finalUrl:
          type:
            - string
            - 'null'
        reportMarkdown:
          type:
            - string
            - 'null'
          description: >-
            The human-readable report. The machine-readable `VERDICT: {...}`
            trailer the model emits is stripped before persistence, so it is not
            present here.
        confidenceScore:
          type:
            - integer
            - 'null'
        stepsJson:
          type:
            - array
            - 'null'
          description: >-
            Per-step execution records. Each entry carries at least the step's
            `action` / `target` / `value` plus its result; entries with a stored
            screenshot are augmented on read with `failureScreenshotUrl` and/or
            `actualScreenshotUrl`. Values for password-like fields are redacted
            before persistence.
          items:
            type: object
            additionalProperties: true
        screenshotRefsJson:
          type:
            - 'null'
            - array
            - object
          description: Screenshot references captured during the run.
        finalScreenshotR2Key:
          type:
            - string
            - 'null'
        finalScreenshotUrl:
          type:
            - string
            - 'null'
          description: Public or presigned URL for the final screenshot.
        networkEventsJson:
          type:
            - array
            - 'null'
          description: >-
            Capped at roughly 200 entries. Null for runs where nothing
            interesting happened.
          items:
            type: object
            additionalProperties: true
        consoleEventsJson:
          type:
            - array
            - 'null'
          description: >-
            Console and page-error events, capped at roughly 200 entries.
            Entries carry at least `level` and `text`.
          items:
            type: object
            additionalProperties: true
        videoR2Key:
          type:
            - string
            - 'null'
        videoUrl:
          type:
            - string
            - 'null'
          description: >-
            Public or presigned URL for the WebM recording. Unlike the list
            shape, this is a real URL here.
        traceR2Key:
          type:
            - string
            - 'null'
        traceUrl:
          type:
            - string
            - 'null'
          description: Public or presigned URL for the Playwright trace zip.
        accessibilityViolationsJson:
          type:
            - array
            - 'null'
          description: >-
            axe-core violations on the final page; entries carry `id` and
            `impact`. Null when the scan did not run.
          items:
            type: object
            additionalProperties: true
        viewportPreset:
          type:
            - string
            - 'null'
        batchId:
          type:
            - string
            - 'null'
          format: uuid
        browser:
          type:
            - string
            - 'null'
        attemptIndex:
          type:
            - integer
            - 'null'
        ticketTitle:
          type:
            - string
            - 'null'
          description: Cached issue-tracker ticket title, if one was ever generated.
        ticketMarkdown:
          type:
            - string
            - 'null'
        ticketGeneratedAt:
          type:
            - string
            - 'null'
          format: date-time
        performanceMetricsJson:
          type:
            - object
            - 'null'
          description: >-
            Final-page timings. Documented shape: `{ lcpMs, fcpMs, ttfbMs,
            domContentLoadedMs, loadEventMs }`.
          additionalProperties: true
        perPagePerformanceJson:
          type:
            - array
            - 'null'
          description: >-
            Per-navigation timeline; each entry adds `url` and `capturedAt` to
            the metrics above.
          items:
            type: object
            additionalProperties: true
        visualResultsJson:
          type:
            - array
            - 'null'
          description: >-
            Visual-regression results, one per step that had a baseline.
            Documented shape: `{ stepIndex, baselineR2Key, actualR2Key,
            diffR2Key, diffPixelCount, diffPercent, threshold, passed }`,
            augmented on read with `baselineUrl`, `actualUrl` and `diffUrl`.
          items:
            type: object
            additionalProperties: true
        downloadsJson:
          type:
            - array
            - 'null'
          description: >-
            Files downloaded during the run. Documented shape: `{ filename,
            sizeBytes, contentType, r2Key, truncated }`.
          items:
            type: object
            additionalProperties: true
        parentRunId:
          type:
            - string
            - 'null'
          format: uuid
        runKind:
          type:
            - string
            - 'null'
        perfStatus:
          type:
            - string
            - 'null'
          description: >-
            `ok` or `degraded` against the procedure's performance budget. Null
            when no budget is configured. Free text in the DB.
        llmTokensUsed:
          type: integer
        llmCostCents:
          type: integer
          description: Integer cents. Money is never stored as a float.
        browserMinutesUsed:
          type: integer
          description: >-
            **Tenths of a minute**, not minutes. The underlying column is
            `browser_minutes_used_tenths`; the JSON key drops the suffix but the
            unit does not change. Divide by 10 for minutes.
        createdAt:
          type: string
          format: date-time
        startedAt:
          type:
            - string
            - 'null'
          format: date-time
        completedAt:
          type:
            - string
            - 'null'
          format: date-time
        lastHeartbeatAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Liveness ping, refreshed every 60 s while the run is alive. The
            stale-runs sweeper cancels a run whose heartbeat is older than 3
            minutes.
        maxDurationMinutes:
          type:
            - integer
            - 'null'
          description: >-
            The per-run cap as requested at trigger time. Null means the
            procedure's value (or the 10-minute default) applied.
    Error:
      type: object
      title: Error
      description: >-
        The universal error envelope. Every failing handler in this surface
        responds with a JSON object carrying a human-readable `error` string;
        some add extra fields (see the other error schemas).
      required:
        - error
      properties:
        error:
          type: string
          description: Human-readable message. Not a stable machine-readable code.
      additionalProperties: true
    UserExpectation:
      type: string
      title: UserExpectation
      enum:
        - pass
        - fail
        - unknown
      default: pass
      description: |
        How to frame the verdict.

        * `pass` (default): a positive test; every step should succeed.
        * `fail`: a negative test; the run "passes" when the application
          correctly refuses the action. A safety net in the orchestrator
          appends a synthetic failed step if nothing actually failed.
        * `unknown`: exploratory; no strong framing.

        The server coerces anything other than `fail`/`unknown` to `pass`.
    RunStatus:
      type: string
      title: RunStatus
      enum:
        - pending
        - running
        - completed
        - failed
        - cancelled
      description: >-
        Lifecycle state. `completed`, `failed` and `cancelled` are terminal;
        `pending` and `running` mean keep polling. Note `completed` means the
        run finished, not that it passed. Read `overallStatus` for that.
    OverallStatus:
      type: string
      title: OverallStatus
      enum:
        - passed
        - failed
      description: >-
        The QA verdict, null until the run finalizes. Derived by
        `deriveOverallStatus`: `passed` only when every non-skipped step passed,
        no coverage gaps remain, and the report's own verdict does not disagree.
    RateLimitError:
      type: object
      title: RateLimitError
      description: Emitted by the rate-limit middleware alongside a `Retry-After` header.
      required:
        - error
      properties:
        error:
          type: string
          description: The limiter's reason, e.g. `Rate limit exceeded. Try again in 42s.`
        retryAfter:
          type:
            - integer
            - 'null'
          description: >-
            Seconds until the window frees. Mirrors `Retry-After`. May be
            absent/null when the limiter did not report a reset time, in which
            case the header defaults to 60.
      additionalProperties: true
  responses:
    Unauthorized:
      description: |
        No `Authorization: Bearer …` header, or the credential did not
        verify. Distinct messages the code can return:

        * `Missing or invalid Authorization header`
        * `Invalid or revoked API key`
        * `API key owner not found`
        * `API key's organization is no longer accessible`. The key's user
          has been removed from the org it is bound to
        * `Invalid token` / `Token missing sub claim`. Clerk JWT path

        Repeated failures burn the `auth:strict` bucket (5 per 15 minutes
        per IP) and then return `429` instead.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Invalid or revoked API key
    RateLimited:
      description: >-
        Throttled. Every `/api/*` route sits behind `api:global` (600 / minute
        per IP); some routes add a tighter per-user limiter.
      headers:
        Retry-After:
          description: Seconds until the window frees up.
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: tst_live_<32 url-safe base64 chars>
      description: |
        Send `Authorization: Bearer tst_live_…`.

        Format (`services/api-keys.ts`): the literal prefix `tst_live_`
        followed by 24 random bytes rendered as 32 base64url characters.
        Only the SHA-256 hash is stored server-side. The shape check that
        routes a token down the API-key path rather than the Clerk JWT path
        requires the `tst_live_` prefix and a total length of at least 25
        characters.

        Keys are minted in the dashboard at `/settings/team`. The same header
        also accepts a Clerk session JWT, which is how the web app
        authenticates, but the JWT path is out of scope for this document.

````

## Related topics

- [Screenshots, logs and evidence](/test-execution/evidence.md)
- [Run and inspect a test](/getting-started/run-and-inspect.md)
- [Reusable flows](/test-creation/reusable-flows.md)
- [Create a project](/getting-started/create-a-project.md)
- [Quickstart](/getting-started/quickstart.md)
