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 |
|---|---|---|---|
| Free | 30 | 50 | 7 days |
| Pro | 90 | 100 | 30 days |
| Team | 180 | 100 | 90 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.