BuildPulse Submissions API

Test result sets (a.k.a. submissions) — one per CI run.

Operations 2

GET /api/repos/{owner}/{name}/submissions List recent test submissions for a repository #
GET /api/repos/{owner}/{name}/submissions/{submission_id}/tests List per-test results for a single submission #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/buildpulse-io:buildpulse-io-submissions-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

buildpulse-io-submissions-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: BuildPulse Platform Submissions API
  version: 1.0.0
  summary: Public REST API for BuildPulse CI analytics.
  termsOfService: https://buildpulse.io/terms
  description: 'The BuildPulse Platform API exposes read-only test analytics for any

    repository onboarded to BuildPulse: paginated test submissions,

    disruptive/flaky test inventory, per-test disruption history, and SVG

    badges for flakiness and coverage. This API is the drop-in replacement for the legacy Rails API at

    `https://buildpulse.io/api`. Endpoint paths, query parameters, and

    response JSON shapes are intentionally identical so existing

    integrations migrate by changing only the base URL. ## Base URLs


    - Production: `https://platform.buildpulse.io`

    - Development: `https://platform.dev.buildpulse.io`


    ## Authentication


    All endpoints except `/health` require an API token in the

    `Authorization` header. Both `token ` and `Bearer `

    prefixes are accepted. Two token shapes are accepted:


    - **New** (`bp_`) — minted by the BuildPulse web app under

    Organization Settings → API Tokens. SHA256-hashed (lowercase hex)

    and stored in `apiTokens` as

    `{ type: "api_token", tokenPrefix, tokenHash, expiresAt }`. Plan-gated: the organization needs an active plan that includes

    API access (Team, Growth, custom, or a paid legacy Marketplace

    plan; the 14-day trial counts). The same gate applies to the

    hosted MCP''s `mcpSessions`. - **Legacy** (``) — Rails-era format, stored as

    `{ hashedToken: base64(SHA256(token)) }`. Gated against

    `marketplacePurchases.unitCount > 0` for parity with the legacy

    API. Production currently holds zero of these; the path is

    retained for any future migrated tokens. The plaintext token is shown only once at creation time. Example values (do not use):


    ```

    Authorization: Bearer bp_14f56e0c2a8b4d1f9e6a3c7b8d2e5f1a0b9c4d7e6f3a2b1c0d9e8f7a6b5c4d3e

    Authorization: token a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0

    ```


    ## Response shapes


    JSON timestamps are ISO 8601 with second precision in UTC

    (e.g. `2026-05-17T12:34:56Z`). List endpoints always return an array

    for the data field, never `null`. Empty results return `[]`. ## Errors


    | Status | Body |

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

    | 401 | empty |

    | 404 | `{"message":"Not Found"}` |

    | 400 | `{"errors":{"after":[""]}}` |

    | 500 | empty |


    The empty-body 401/500 contract matches the legacy Rails behavior

    (`render body: nil`) and is preserved deliberately. ## Pagination


    Two cursor styles are in use, both base64-encoded so consumers should

    treat them as opaque:


    - **Keyset (submissions only)** — cursor is `base64("id:")`. Stable under inserts. - **Offset (tests, flaky tests)** — cursor is `base64("offset:")`. The next-page cursor is returned as `metadata.after`. When there is

    no next page, `metadata.after` is `null`. ## Reporting window


    Time-bounded endpoints use a rolling window ending now. Which window

    depends on the endpoint:


    | Endpoint | Window |

    |---|---|

    | `GET /api/v1/flaky/tests?quarantine=false` | Your plan''s reporting period (default 14 days) |

    | `GET /api/tests/{id}/results` | Your plan''s reporting period (default 14 days) |

    | `GET /api/v1/flaky/tests` (quarantined view, the default) | Not windowed — every quarantined test is returned |

    | `GET /api/repos/{owner}/{name}/tests` | Not windowed — every test with a disruptiveness ratio above 0 |

    | `GET /api/repos/{owner}/{name}/submissions` | Fixed 14 days |

    | `GET /api/v1/flaky/badges` | Fixed 14 days |


    The plan''s reporting period is resolved per organization from its

    active plan, so it follows plan changes automatically; there is no

    request parameter to override it. Organizations without a resolvable

    plan get the 14-day default. Don''t hardcode a window client-side —

    treat the returned rows as the source of truth. ## For AI agents (MCP)


    The BuildPulse MCP server exposes this API as nine read-only tools

    for Claude, ChatGPT, Cursor, VS Code, and any Model Context

    Protocol client. Hosted: `https://mcp.buildpulse.io/mcp`

    (Streamable HTTP, OAuth sign-in with your BuildPulse account). Stdio: `npx -y @buildpulse/mcp` with `BUILDPULSE_TOKEN` set to an

    API token. Setup and tool list:

    https://platform.buildpulse.io/docs/mcp.'
  license:
    name: Proprietary
    url: https://buildpulse.io/terms
  contact:
    name: BuildPulse Support
    email: support@buildpulse.io
    url: https://buildpulse.io
servers:
- url: https://platform.buildpulse.io
  description: Production
- url: https://platform.dev.buildpulse.io
  description: Development
security:
- TokenAuth: []
- BearerAuth: []
tags:
- name: Submissions
  description: Test result sets (a.k.a. submissions) — one per CI run.
paths:
  /api/repos/{owner}/{name}/submissions:
    get:
      operationId: listSubmissions
      tags:
      - Submissions
      summary: List recent test submissions for a repository
      description: 'Returns the last 14 days of test result sets ("submissions") for

        the named repository, newest first. Pagination is keyset-based on

        the document ID — `metadata.after` is an opaque cursor that

        should be passed back as the `after` query parameter to fetch the

        next page.


        Repository lookup is case-insensitive; `OWNER/REPO` matches

        `owner/repo`.'
      parameters:
      - $ref: '#/components/parameters/Owner'
      - $ref: '#/components/parameters/Name'
      - $ref: '#/components/parameters/Limit10'
      - $ref: '#/components/parameters/AfterCursor'
      responses:
        '200':
          description: Paginated submissions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmissionsResponse'
              example:
                count: 2
                submissions:
                - key: 1234/5678/test-results.xml.gz
                  build_url: https://github.com/acme/widgets/actions/runs/1234
                  commit_oid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
                  recorded_at: '2026-05-17T12:34:56Z'
                  status: processed
                  test_result_count: 412
                - key: 1234/5679/test-results.xml.gz
                  build_url: https://github.com/acme/widgets/actions/runs/1235
                  commit_oid: b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0a1
                  recorded_at: '2026-05-17T11:00:00Z'
                  status: processed
                  test_result_count: 412
                metadata:
                  after: aWQ6NjVkNGYxYThiM2MyZDFlMGY5YThiN2M2
                  limit: 10
        '400':
          $ref: '#/components/responses/BadCursor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimitExceeded'
        '404':
          $ref: '#/components/responses/NotFound'
      x-codeSamples:
      - lang: shell
        label: curl
        source: "curl -H \"Authorization: token $BUILDPULSE_TOKEN\" \\\n  \"https://platform.buildpulse.io/api/repos/acme/widgets/submissions?limit=5\"\n"
      - lang: javascript
        label: Node
        source: "const res = await fetch(\n  \"https://platform.buildpulse.io/api/repos/acme/widgets/submissions?limit=5\",\n  { headers: { Authorization: `token ${process.env.BUILDPULSE_TOKEN}` } }\n);\nconst { submissions, metadata } = await res.json();\n"
      - lang: python
        label: Python
        source: "import os, requests\nr = requests.get(\n  \"https://platform.buildpulse.io/api/repos/acme/widgets/submissions\",\n  headers={\"Authorization\": f\"token {os.environ['BUILDPULSE_TOKEN']}\"},\n  params={\"limit\": 5},\n)\nr.raise_for_status()\ndata = r.json()\n"
  /api/repos/{owner}/{name}/submissions/{submission_id}/tests:
    get:
      operationId: listSubmissionTests
      tags:
      - Submissions
      summary: List per-test results for a single submission
      description: 'Returns the individual test results recorded against one

        submission (one CI run). Use `status=failed` to filter to just

        the failures/errors — useful when triaging a red test run.


        The submission identifier is the 24-char hex `id` returned by

        `GET /api/repos/{owner}/{name}/submissions`. Each row carries

        `conclusion` (`passing`/`failure`/`error`/`skipped`),

        `duration_us` (per-attempt microseconds), `message` (failure

        message, may be null), and the parent test case''s name, suite,

        file, and classname.


        Repository lookup is case-insensitive; `OWNER/REPO` matches

        `owner/repo`. The submission must belong to a repository the

        caller''s token can access — otherwise a 404 is returned (no

        information is leaked about cross-tenant submissions).'
      parameters:
      - $ref: '#/components/parameters/Owner'
      - $ref: '#/components/parameters/Name'
      - name: submission_id
        in: path
        required: true
        schema:
          type: string
        description: Submission identifier (24-char hex ObjectID).
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - all
          - failed
          - passed
          - failure
          - error
          - skipped
        description: 'Optional conclusion filter. `failed` matches both

          `failure` and `error` (the typical "red test run" set).

          Omit or pass `all` to return every result.

          '
      - $ref: '#/components/parameters/Limit25'
      - $ref: '#/components/parameters/AfterCursor'
      responses:
        '200':
          description: Paginated test results for the submission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmissionTestsResponse'
              example:
                count: 3
                tests:
                - id: 65d4f1a8b3c2d1e0f9a8b7c6
                  test_case_id: 65d4f1a8b3c2d1e0f9a8b7c0
                  name: logs in with valid credentials
                  suite: Authentication
                  class: AuthSpec
                  file: spec/auth_spec.rb
                  conclusion: failure
                  duration_us: 1245000
                  message: expected 200 but got 500
                  ran_at: '2026-05-27T18:42:11Z'
                  run_count: 3
                metadata:
                  after: null
                  limit: 25
        '400':
          $ref: '#/components/responses/BadCursor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PlanLimitExceeded'
        '404':
          $ref: '#/components/responses/NotFound'
      x-codeSamples:
      - lang: shell
        label: curl
        source: "curl -H \"Authorization: token $BUILDPULSE_TOKEN\" \\\n  \"https://platform.buildpulse.io/api/repos/acme/widgets/submissions/65d4f1a8b3c2d1e0f9a8b7c6/tests?status=failed\"\n"
components:
  schemas:
    NotFoundError:
      type: object
      required:
      - message
      properties:
        message:
          type: string
          enum:
          - Not Found
    Submission:
      type: object
      required:
      - id
      - key
      - build_url
      - commit_oid
      - recorded_at
      - status
      - test_result_count
      properties:
        id:
          type: string
          description: 'Submission identifier (24-char hex ObjectID). Pass to

            `/api/repos/{owner}/{name}/submissions/{submission_id}/tests`

            to fetch the per-test results for this submission.

            '
          example: 65d4f1a8b3c2d1e0f9a8b7c6
        key:
          type: string
          description: 'Legacy S3 object key for the raw test result archive. Stable

            and unique per submission in the legacy stack; empty string

            on the new pipeline. Prefer `id` for cross-endpoint joins.

            '
          example: 1234/5678/test-results.xml.gz
        build_url:
          type: string
          format: uri
          description: CI run URL that produced this submission.
        commit_oid:
          type: string
          description: Git commit SHA-1 (40 hex chars).
        recorded_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp (UTC, second precision).
        status:
          type: string
          description: 'Processing status. Currently `processed`, `processing`, or

            other internal states.

            '
          example: processed
        test_result_count:
          type: integer
          description: 'Number of individual test results in this submission. Counts every

            recorded attempt — retries and skipped tests included — so it can

            exceed the number of distinct tests in the run.

            '
    SubmissionTest:
      type: object
      required:
      - id
      - test_case_id
      - name
      - suite
      - class
      - file
      - conclusion
      - duration_us
      - message
      - body
      - ran_at
      - run_count
      properties:
        id:
          type: string
          description: Test result identifier (24-char hex ObjectID).
        test_case_id:
          type: string
          description: 'Parent test case identifier (24-char hex ObjectID). Two

            results with the same `test_case_id` are different runs of

            the same logical test (e.g. retries within one submission).

            '
        name:
          type: string
        suite:
          type: string
        class:
          type: string
        file:
          type: string
        conclusion:
          type: string
          enum:
          - passing
          - failure
          - error
          - skipped
          - unknown
          description: 'Outcome of this attempt. `failed` in the `status` filter

            matches both `failure` and `error`.

            '
        duration_us:
          type: integer
          format: int64
          description: Per-attempt duration in microseconds.
        message:
          type:
          - string
          - 'null'
          description: 'One-line failure or error summary as recorded by the runner;

            null for passing / skipped.

            '
        body:
          type:
          - string
          - 'null'
          description: 'Full failure detail (assertion + stack trace); null for

            passing / skipped. Truncated at 2000 bytes on the wire

            (UTF-8 safe, never split mid-codepoint); the authoritative

            copy lives in the underlying testResults row.

            '
        ran_at:
          type: string
          format: date-time
        run_count:
          type: integer
          description: 'Retry attempt number recorded by the test runner. `1` for

            the first attempt, `2` for the first retry, etc.

            '
    PlanLimitError:
      type: object
      required:
      - code
      - message
      - upgradeUrl
      properties:
        code:
          type: string
          enum:
          - plan_limit_exceeded
          description: Stable machine-readable code; consumers key off this, not the message.
        message:
          type: string
          description: Human-readable explanation.
        upgradeUrl:
          type: string
          format: uri
          description: Deep link to billing/upgrade so access can be restored.
    SubmissionTestsResponse:
      type: object
      required:
      - count
      - tests
      - metadata
      properties:
        count:
          type: integer
          format: int64
          description: Total matching test results (not just this page).
        tests:
          type: array
          items:
            $ref: '#/components/schemas/SubmissionTest'
        metadata:
          $ref: '#/components/schemas/Metadata'
    CursorError:
      type: object
      required:
      - errors
      properties:
        errors:
          type: object
          required:
          - after
          properties:
            after:
              type: array
              items:
                type: string
    SubmissionsResponse:
      type: object
      required:
      - count
      - submissions
      - metadata
      properties:
        count:
          type: integer
          format: int64
          description: Total submissions matching the filter (not just this page).
        submissions:
          type: array
          items:
            $ref: '#/components/schemas/Submission'
        metadata:
          $ref: '#/components/schemas/Metadata'
    Metadata:
      type: object
      required:
      - after
      - limit
      properties:
        after:
          type:
          - string
          - 'null'
          description: Opaque cursor for the next page; `null` if there is no next page.
        limit:
          type: integer
  parameters:
    Owner:
      in: path
      name: owner
      required: true
      schema:
        type: string
      description: Repository owner (case-insensitive).
      example: acme
    Limit25:
      in: query
      name: limit
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
      description: Page size. Clamped to `[1, 100]`.
    Name:
      in: path
      name: name
      required: true
      schema:
        type: string
      description: Repository name (case-insensitive).
      example: widgets
    Limit10:
      in: query
      name: limit
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 10
      description: Page size. Clamped to `[1, 100]`.
    AfterCursor:
      in: query
      name: after
      schema:
        type: string
      description: 'Opaque pagination cursor from a previous response''s

        `metadata.after`. Pass back verbatim.

        '
  responses:
    BadCursor:
      description: Cursor failed to decode.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/CursorError'
          example:
            errors:
              after:
              - invalid cursor format
    PlanLimitExceeded:
      description: 'The organization is over its plan''s monthly test-result limit and

        data-read access is restricted (staged-overage stage `soft` or

        `hard`). At `soft` the data is locked but safe — ingestion keeps

        recording. At `hard` ingestion is also paused (raw retained ~30

        days) and an upgrade resumes processing. Read `GET /api/v1/usage`

        for the current stage and usage, and visit `upgradeUrl` to restore

        access. Only returned for organizations positively identified as

        over the enforced limit while enforcement is enabled.

        '
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/PlanLimitError'
          example:
            code: plan_limit_exceeded
            message: Your organization has exceeded its plan's monthly test-result limit. Your data is safe but locked — upgrade your plan to restore access.
            upgradeUrl: https://buildpulse.io/settings/billing
    Unauthorized:
      description: 'Missing, malformed, or invalid token; or — for legacy 40-hex

        tokens only — the organization has no active plan

        (`marketplacePurchases.unitCount > 0`). The new `bp_<64-hex>`

        path has no plan gate, so a 401 there means the token shape

        was wrong, the hash didn''t match, or the token has expired.

        Response body is empty (matches legacy Rails).

        '
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/NotFoundError'
          example:
            message: Not Found
  securitySchemes:
    TokenAuth:
      type: apiKey
      in: header
      name: Authorization
      description: "Send `Authorization: token <token>`. Two token shapes are\naccepted:\n\n- **New** (`bp_<64-hex>`) — minted by the refactored web-client\n  today (`web-client/src/app/_services/apiTokens.ts`). Stored as\n  `{ type: \"api_token\", tokenPrefix, tokenHash (hex SHA256),\n  expiresAt }`. No active-plan gate; issuance is the\n  entitlement check.\n- **Legacy** (`<40-hex>`) — Rails-era format, stored as\n  `{ hashedToken: base64(SHA256(token)) }`. Plan-gated against\n  `marketplacePurchases.unitCount > 0`. Retained for any future\n  migrated tokens.\n\nThe legacy API only accepted `token`, so existing integrations\nneed no changes.\n"
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: bp_<64-hex> or <40-hex>
      description: 'Send `Authorization: Bearer <token>`. Functionally identical to

        `TokenAuth`; both header forms are accepted. Same two token

        shapes apply — see `TokenAuth` for the format and plan-gate

        details.

        '
externalDocs:
  description: BuildPulse docs, MCP server and guides
  url: https://platform.buildpulse.io/docs/mcp