← Back to Docs

Gaffer API Reference

The Gaffer Public API v1 is a read-first REST API over your test history: runs, failures, flaky tests, durations, coverage, and upload status. It is served from https://app.gaffer.sh, authenticated with an X-API-Key header, and returns JSON.

The v1 surface is 22 endpoints: 21 GET reads and one POST ingest. This is the same API that Gaffer's own tooling runs on. The MCP server puts 16 of those reads behind a codemode namespace that agents call through its execute_code tool, plus list_projects as a first-class tool, and gaffer test posts every local run to POST /api/v1/ingest. If a capability exists in the CLI or in an AI assistant's Gaffer tools, it is reachable here with curl.

The interactive endpoint browser, with every parameter, schema, and example response, is at the bottom of this page. What follows is the part the spec does not tell you: which key to use, what failures look like, and where the API fits next to the CLI and the GitHub Action.

How do I authenticate with the Gaffer API?

Send your key in the X-API-Key header. Gaffer accepts two key types through that same header, distinguished by prefix. Bearer tokens and query-string keys are not supported.

Key type Format Scope Where to create it
Project token gfr_ + 64 hex One project Project settings, Project Tokens tab
User API key gaf_ + 64 hex Every project in your organizations app.gaffer.sh/account/api-keys
# Project token: the project is inferred from the token
curl -H "X-API-Key: gfr_your_project_token" \
  https://app.gaffer.sh/api/v1/project/analytics

# User API key: name the project explicitly
curl -H "X-API-Key: gaf_your_user_api_key" \
  https://app.gaffer.sh/api/v1/user/projects

Which key should I use?

Situation Use Why
CI pipeline uploading reports Project token A leaked CI secret compromises one project, not the organization
Coding agent working in one repo Project token No project ID to configure, and no way to read a sibling project
Cross-project dashboard or bot User API key Only key type that can call GET /api/v1/user/projects
Local experimentation Either Both accept the same endpoints once a project ID is known

Project tokens are enforced, not merely scoped. Calling /api/v1/user/projects/OTHER_ID/health with a token belonging to a different project returns 403 rather than silently reading the token's own project.

What is the base URL and how is the API versioned?

The base URL is https://app.gaffer.sh and every v1 endpoint lives under /api/v1. The version is in the path, so there is no negotiation header and no separate API subdomain to configure.

Path prefix Auth Purpose
/api/v1/project/* Project token Project-scoped reads with no project ID in the URL
/api/v1/user/projects/* Either key type The main read surface: health, runs, flaky tests, failures, coverage
/api/v1/ingest Project token Structured run payloads from the CLI, deduplicated by runId
/api/upload Project token Multipart file upload, outside v1. See the Upload API.

Are there rate limits?

Gaffer does not publish a fixed per-key request quota for the v1 read API. Two limits do apply in practice, and both are worth handling in client code.

Plan-based clamping. Analytics endpoints take a days parameter that is silently reduced to your plan's ceiling. The response reports what actually happened.

Plan Max analysis window (days) Max test-history entries Report retention
Free30507 days
Pro9010030 days
Team18010090 days
{
  "meta": {
    "appliedDays": 30,
    "requestedDays": 90,
    "clampedByPlan": true
  }
}

The meta object is only present when clamping occurred. Read appliedDays (or summary.period) instead of assuming the window you requested is the window you got. A chart labelled "last 90 days" that is actually showing 30 is the most common integration bug against this API.

Throttling. Treat HTTP 429 as retryable and honor the Retry-After header, which carries seconds until the window resets. Requests can be throttled at the edge under load even where no per-key quota is published.

What does an error response look like?

Errors return a JSON envelope with the HTTP status repeated in the body and a human-readable reason in statusMessage. Here is a real response from production, for a request sent with a malformed key:

$ curl -i -H "X-API-Key: bogus_key" https://app.gaffer.sh/api/v1/user/projects

HTTP/2 401
content-type: application/json

{
  "error": true,
  "url": "https://app.gaffer.sh/api/v1/user/projects",
  "statusCode": 401,
  "statusMessage": "Invalid API key format. Expected user API key (gaf_...).",
  "message": ""
}

Branch on statusCode, and log statusMessage. The message field is reserved for detail that most errors do not carry, so it is frequently an empty string. Cloudflare also returns a cf-ray header on every response: include it when contacting support, because it is what identifies your specific request in the logs.

Status Typical cause Retry?
400 Invalid query parameter, or a user API key used without a project ID No. Fix the request.
401 Missing X-API-Key, unrecognized prefix, or a revoked key No. Check the key.
403 Valid key, wrong project. Common with project tokens. No.
404 Project, run, or report does not exist, or was aged out by retention No.
429 Throttled Yes, after Retry-After seconds.
5xx Server or upstream error Yes, with exponential backoff.

A 404 on a run that worked last month is usually retention rather than a bug. Check the retention column in the table above before debugging your client.

REST API, CLI, or GitHub Action?

These are three layers over one service, not three competing products. Upload from CI with the Action, run and query tests locally with the CLI, and call the REST API when you are building something neither covers.

REST API CLI (gaffer) GitHub Action
Best for Custom dashboards, bots, scripts Local runs, terminal queries, coding agents Uploading reports from CI
Runs your tests No Yes, gaffer test -- your-command No, uploads what CI produced
Talks to /api/v1/* /api/v1/ingest and the multipart endpoints /api/upload and the multipart endpoints
Max file size 75 MB per request (single POST) 5 GB per file 5 GB per file
Credential X-API-Key GAFFER_PROJECT_TOKEN gaffer_upload_token input

The usual CI setup is the Action, which needs one step:

- name: Upload test reports to Gaffer
  if: always()
  uses: gaffer-sh/gaffer-uploader@v2
  with:
    gaffer_upload_token: ${{ secrets.GAFFER_PROJECT_TOKEN }}
    report_path: test-results

Then read the results back from the API in a later step, or from an agent through the MCP server. A common pairing is uploading with the Action and querying /health from a PR comment job:

curl -s -H "X-API-Key: $GAFFER_PROJECT_TOKEN" \
  "https://app.gaffer.sh/api/v1/user/projects/$PROJECT_ID/health?days=30" \
  | jq '.analytics | {healthScore, passRate, flakyTestCount, trend}'

The health score that endpoint returns comes from the same function the dashboard uses, so those two always agree. The CLI applies the same 60/30/10 weighting against different inputs, so its number can differ. The Analytics reference documents the formula, its thresholds, and where the CLI diverges.

Frequently asked questions

What is the base URL for the Gaffer API?

https://app.gaffer.sh. Every v1 endpoint lives under /api/v1, for example https://app.gaffer.sh/api/v1/user/projects. There is no separate API subdomain and no version negotiation header: the version is in the path.

How do I authenticate with the Gaffer API?

Send your key in the X-API-Key header. Gaffer accepts two key types with the same header. A project token (gfr_ followed by 64 hex characters) is scoped to one project and is created in that project’s settings under Project Tokens. A user API key (gaf_ followed by 64 hex characters) reaches every project in your organizations and is created at app.gaffer.sh/account/api-keys. Bearer tokens are not supported.

What is the difference between a project token and a user API key?

Scope. A project token resolves to exactly one project, so the API infers the project and rejects any request aimed at a different one with a 403. A user API key spans every project you can access but requires you to name the project ID in the path, and it is the only key type that can call the list-projects endpoint. Use project tokens in CI, where a leaked secret should compromise one project rather than all of them.

Does the Gaffer API have rate limits?

Gaffer does not publish a fixed per-key request quota for the v1 read API. Two limits do apply. Analysis windows are clamped by plan tier: 30 days on Free, 90 on Pro, 180 on Team, with the applied value reported in the response meta object. And clients should treat HTTP 429 as retryable, honoring the Retry-After header, since throttling can be applied at the edge under load.

Should I use the REST API, the CLI, or the GitHub Action?

Use the GitHub Action to upload from CI, the CLI to run tests locally and query results from a terminal or coding agent, and the REST API when you are building something the other two do not cover, such as a custom dashboard or an internal bot. They are layers over the same service, not alternatives: the CLI posts to /api/v1/ingest and the GitHub Action posts to /api/upload.

Full endpoint reference

Every endpoint, parameter, and response schema, generated from the OpenAPI 3.1 spec at /openapi/v1.yaml.