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

# List a project's saved procedures

> Every procedure under the project, ordered by `lastRunAt` descending then `createdAt` descending, so the most recently exercised ones come first. Returns whole rows, including `stepsJson`. Used by `testorim list --project <id>` and by `testorim init` to build its alias map.



## OpenAPI

````yaml /api-reference/openapi.yaml get /api/projects/{projectId}/procedures
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/projects/{projectId}/procedures:
    get:
      tags:
        - Procedures
      summary: List a project's saved procedures
      description: >-
        Every procedure under the project, ordered by `lastRunAt` descending
        then `createdAt` descending, so the most recently exercised ones come
        first. Returns whole rows, including `stepsJson`. Used by `testorim list
        --project <id>` and by `testorim init` to build its alias map.
      operationId: listProcedures
      parameters:
        - $ref: '#/components/parameters/ProjectIdPath'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                required:
                  - procedures
                properties:
                  procedures:
                    type: array
                    items:
                      $ref: '#/components/schemas/Procedure'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/ProjectNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - ApiKeyAuth: []
components:
  parameters:
    ProjectIdPath:
      name: projectId
      in: path
      required: true
      description: Project id (UUID).
      schema:
        type: string
        format: uuid
  schemas:
    Procedure:
      type: object
      title: Procedure
      description: A saved, replayable test sequence. Handlers return the whole DB row.
      required:
        - id
        - projectId
        - name
        - stepsJson
        - runOnMobile
        - runThrice
        - isLoginSetup
        - authMode
        - createdAt
        - runCount
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
          description: >-
            The owning project. This is the field the CLI reads to satisfy the
            trigger endpoint's required `projectId`.
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        stepsJson:
          type: array
          description: >-
            The saved steps. Typed as untyped JSONB in the database, so a legacy
            row could in principle hold another shape.
          items:
            $ref: '#/components/schemas/ProcedureStep'
        runOnMobile:
          type: boolean
          description: >-
            Fan out each run to desktop and mobile viewports. Gated to paid
            plans server-side.
        runThrice:
          type: boolean
          description: Run three times sequentially to surface flakiness.
        browsersJson:
          type:
            - array
            - 'null'
          description: >-
            Cross-browser fan-out. Null or empty means Chromium only. Team plan
            only, enforced server-side.
          items:
            type: string
            enum:
              - chromium
              - firefox
              - webkit
        isLoginSetup:
          type: boolean
          description: >-
            Marks this as the project's authoritative login flow. On success the
            browser's `storageState` is captured onto the project so other
            procedures start pre-authenticated. At most one procedure per
            project carries the flag.
        authMode:
          type: string
          enum:
            - unknown
            - fresh_logged_out
            - reuse_login_state
          description: >-
            Browser-state policy between matrix legs. `fresh_logged_out` starts
            every leg clean (login/signup/logout flows); `reuse_login_state`
            preloads saved login state (dashboard workflows); `unknown`
            preserves legacy behaviour.
        tagsJson:
          type:
            - array
            - 'null'
          description: Free-form lowercase tags, e.g. `smoke`, `regression`.
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        lastRunAt:
          type:
            - string
            - 'null'
          format: date-time
        runCount:
          type: integer
        maxDurationMinutes:
          type:
            - integer
            - 'null'
          description: >-
            Per-procedure time cap in minutes. Null means the platform default
            of 10. The 5..120 bound is enforced by the API, not the column, so
            legacy rows may sit outside it.
    ProcedureStep:
      type: object
      title: ProcedureStep
      description: >-
        One saved step. The persisted array is untyped JSONB, but the write path
        validates this shape (`StepSchema` in `routes/procedures.ts`).
      required:
        - action
        - target
      properties:
        action:
          type: string
          minLength: 1
          maxLength: 40
          description: >-
            e.g. `navigate`, `click`, `type`, `assert`, `prompt_user`. Not
            constrained to an enum at this boundary.
        target:
          type: string
          minLength: 1
          maxLength: 2000
          description: Natural-language element description or URL.
        value:
          type: string
          maxLength: 8000
          description: >-
            Text to type, or the expected value for an assertion. Supports
            `{{token}}` substitution from project variables and built-in helpers
            such as `{{random.email}}`.
      additionalProperties: true
    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
    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
    ProjectNotFound:
      description: >-
        No live project with this id in the key's organization. Archived and
        cross-organization projects are indistinguishable from missing ones.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Project not found
    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

- [Projects](/projects/projects.md)
- [Platform overview](/developers/platform-overview.md)
- [List projects](/api-reference/projects/list-projects.md)
- [List runs for one project](/api-reference/runs/list-runs-for-one-project.md)
- [Create a project](/getting-started/create-a-project.md)
