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-tests-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: 3.2.0
info:
title: BuildPulse Platform Tests 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: Tests
description: Disruptive / flaky tests per repository.
paths:
/api/repos/{owner}/{name}/tests:
get:
operationId: listTests
tags:
- Tests
summary: List disruptive (flaky) tests for a repository
description: 'Returns tests with a disruptiveness ratio above 0, sorted by
disruptiveness ratio (default) or by recency. This endpoint is not
time-windowed.
The `q` parameter accepts a structured query string. Terms are
space-separated and AND''d together. Supported terms:
- `first-seen:[op]YYYY-MM-DD` — operators ``, `>=`
(no operator = exact day match).
- `last-seen:[op]YYYY-MM-DD` — same operator set.
- `tags:foo,bar` — matches any listed tag.
- bare text — partial, case-insensitive match against
`suite`, `name`, `file`, and `classname`.
Example: `q=last-seen:>=2026-05-01 tags:auth login`.'
parameters:
- $ref: '#/components/parameters/Owner'
- $ref: '#/components/parameters/Name'
- $ref: '#/components/parameters/Limit10'
- $ref: '#/components/parameters/AfterCursor'
- $ref: '#/components/parameters/Sort'
- $ref: '#/components/parameters/Q'
responses:
'200':
description: Paginated disruptive tests.
content:
application/json:
schema:
$ref: '#/components/schemas/TestsResponse'
example:
count: 1
tests:
- id: 65d4f1a8b3c2d1e0f9a8b7c6
name: test_login_success
suite: auth
class: AuthTest
file: test/auth_test.rb
disruptiveness: 0.375
nondeterministic_negative_result_count: 3
nondeterminism_first_recorded_at: '2026-05-10T08:00:00Z'
latest_nondeterministic_commit_oid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
latest_nondeterministic_negative_result:
recorded_at: '2026-05-17T10:00:00Z'
build_url: https://github.com/acme/widgets/actions/runs/9001
message: 'AssertionError: expected 200, got 500'
latest_nondeterministic_positive_result:
recorded_at: '2026-05-16T14:30:00Z'
build_url: https://github.com/acme/widgets/actions/runs/8997
message: null
metadata:
after: null
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/tests?q=last-seen:%3E%3D2026-05-01&sort=recency\"\n"
- lang: javascript
label: Node
source: "const url = new URL(\"https://platform.buildpulse.io/api/repos/acme/widgets/tests\");\nurl.searchParams.set(\"q\", \"last-seen:>=2026-05-01\");\nurl.searchParams.set(\"sort\", \"recency\");\nconst res = await fetch(url, {\n headers: { Authorization: `token ${process.env.BUILDPULSE_TOKEN}` },\n});\n"
- lang: python
label: Python
source: "import os, requests\nr = requests.get(\n \"https://platform.buildpulse.io/api/repos/acme/widgets/tests\",\n headers={\"Authorization\": f\"token {os.environ['BUILDPULSE_TOKEN']}\"},\n params={\"q\": \"last-seen:>=2026-05-01\", \"sort\": \"recency\"},\n)\ndata = r.json()\n"
/api/tests/{id}/results:
get:
operationId: listTestResults
tags:
- Tests
summary: List recent disruption events for a specific test
description: 'Returns up to 10 most-recent disruption events for the given
disruptor (test) ID, newest first, within the organization''s
flaky-test reporting window (the plan''s reporting period, default
14 days — the same window as `GET /api/v1/flaky/tests`). The test
must belong to a repository in the caller''s organization.
Each event is one disruptive result: the failing run the detector
flagged, with the CI `build_url` and `commit_oid` of the result set
it came from, its `conclusion` (`failure` or `error`), and the
failure `message` (null when the reporter sent none). Tests flagged
purely by failure ratio fall back to their failing results in the
window when no cohort disruption is recorded.'
parameters:
- in: path
name: id
required: true
schema:
type: string
pattern: ^[0-9a-fA-F]{24}$
description: 'The disruptor (test) ID as a 24-character MongoDB ObjectID
hex string. This is the same `id` returned by
`GET /api/repos/{owner}/{name}/tests` and by
`GET /api/v1/flaky/tests`.
'
responses:
'200':
description: Recent disruption events.
content:
application/json:
schema:
$ref: '#/components/schemas/TestResultsResponse'
example:
results:
- id: 65d4f1a8b3c2d1e0f9a8b7c7
build_url: https://github.com/acme/widgets/actions/runs/9001
commit_oid: a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
conclusion: failure
recorded_at: '2026-05-17T10:00:00Z'
message: 'timeout: expected task to complete in 5s'
'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/tests/65d4f1a8b3c2d1e0f9a8b7c6/results\n"
- lang: javascript
label: Node
source: "const res = await fetch(\n \"https://platform.buildpulse.io/api/tests/65d4f1a8b3c2d1e0f9a8b7c6/results\",\n { headers: { Authorization: `token ${process.env.BUILDPULSE_TOKEN}` } }\n);\nconst { results } = await res.json();\n"
- lang: python
label: Python
source: "import os, requests\ntest_id = \"65d4f1a8b3c2d1e0f9a8b7c6\"\nr = requests.get(\n f\"https://platform.buildpulse.io/api/tests/{test_id}/results\",\n headers={\"Authorization\": f\"token {os.environ['BUILDPULSE_TOKEN']}\"},\n)\nresults = r.json()[\"results\"]\n"
components:
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
schemas:
TestsResponse:
type: object
required:
- count
- tests
- metadata
properties:
count:
type: integer
format: int64
tests:
type: array
items:
$ref: '#/components/schemas/Test'
metadata:
$ref: '#/components/schemas/Metadata'
TestResult:
type: object
required:
- id
- build_url
- commit_oid
- conclusion
- recorded_at
- message
properties:
id:
type: string
build_url:
type: string
format: uri
commit_oid:
type: string
conclusion:
type: string
description: Disruption conclusion — `failure` or `error`.
recorded_at:
type: string
format: date-time
message:
type:
- string
- 'null'
CursorError:
type: object
required:
- errors
properties:
errors:
type: object
required:
- after
properties:
after:
type: array
items:
type: string
TestResultsResponse:
type: object
required:
- results
properties:
results:
type: array
items:
$ref: '#/components/schemas/TestResult'
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
NotFoundError:
type: object
required:
- message
properties:
message:
type: string
enum:
- Not Found
Result:
type: object
required:
- recorded_at
- build_url
- message
properties:
recorded_at:
type: string
format: date-time
build_url:
type: string
format: uri
message:
type:
- string
- 'null'
description: 'Failure message (negative result) or always `null`
(positive result, to match legacy behavior).
'
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.
Test:
type: object
required:
- id
- name
- suite
- class
- file
- disruptiveness
- nondeterministic_negative_result_count
- nondeterminism_first_recorded_at
- latest_nondeterministic_commit_oid
- latest_nondeterministic_negative_result
- latest_nondeterministic_positive_result
properties:
id:
type: string
description: 24-character MongoDB ObjectID hex.
name:
type: string
suite:
type: string
class:
type: string
file:
type: string
disruptiveness:
type: number
format: float
description: 'Disruptiveness ratio, truncated to 3 decimal places.
Ranges from 0.0 (deterministic) to 1.0 (always flakes).
'
nondeterministic_negative_result_count:
type: integer
nondeterminism_first_recorded_at:
type: string
format: date-time
latest_nondeterministic_commit_oid:
type: string
latest_nondeterministic_negative_result:
$ref: '#/components/schemas/Result'
latest_nondeterministic_positive_result:
$ref: '#/components/schemas/Result'
parameters:
Owner:
in: path
name: owner
required: true
schema:
type: string
description: Repository owner (case-insensitive).
example: acme
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]`.
Q:
in: query
name: q
schema:
type: string
description: 'Structured query string. See `/api/repos/{owner}/{name}/tests`
description for the full grammar.
'
example: last-seen:>=2026-05-01 tags:auth login
Sort:
in: query
name: sort
schema:
type: string
enum:
- disruptivenessRatio
- recency
default: disruptivenessRatio
description: 'Sort order. `recency` sorts by `latestDisruptionAt` descending.
'
AfterCursor:
in: query
name: after
schema:
type: string
description: 'Opaque pagination cursor from a previous response''s
`metadata.after`. Pass back verbatim.
'
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