---
title: "Ingest structured test data"
method: POST
path: "/api/v1/ingest"
tags: ["Ingest"]
---

# Ingest structured test data

`POST /api/v1/ingest`

## Request

- Base URL: `https://app.gaffer.sh`
- URL: `https://app.gaffer.sh/api/v1/ingest`
- Auth: one of:
  - API key in header `X-API-Key`
  - API key in header `X-API-Key`
  - HTTP bearer

Receives structured test data from the Gaffer CLI, the GitHub Action, or
the MCP server's `upload_test_results` function.
The payload is stored in R2 and processed asynchronously via a queue consumer.

Returns 202 immediately with an `uploadSessionId`. Poll the upload-sessions
endpoint to check when processing completes.

**This endpoint is not idempotent.** Each request creates a new upload and,
once processed, a new test run. `runId` is stored as the report's source key
and does not deduplicate across requests, so retrying a call that may have
already succeeded produces a duplicate run. Poll the upload-sessions endpoint
instead of retrying blind.

**Rate limited per project** across all callers. Exceeding the limit returns
429 with a `Retry-After` header. `X-RateLimit-*` headers are set on every
response.

Accepts either credential type. A project token (`gfr_`) is already scoped to
one project and ignores `projectId`; a user API key (`gaf_`) is not, and must
pass `projectId`.

## Query parameters

- `projectId` string

## Request body

- IngestPayload
  - `runId` string, uuid, required: Local UUID used for deduplication
  - `framework` string, required: Test framework identifier (e.g. "vitest", "playwright", "jest")
  - `branch` string: Git branch name
  - `commitSha` string: Git commit SHA
  - `ciProvider` string: CI provider (e.g. "github-actions")
  - `startedAt` string, date-time, required: ISO 8601 timestamp when the run started
  - `finishedAt` string, date-time, required: ISO 8601 timestamp when the run finished
  - `summary` object, required
    - `total` integer, required
    - `passed` integer, required
    - `failed` integer, required
    - `skipped` integer, required
    - `durationMs` number, required
  - `tests` object[], required
    - `name` string, required
    - `status` 'passed' | 'failed' | 'skipped' | 'todo', required
    - `durationMs` number, required
    - `filePath` string
    - `error` string
    - `retryCount` integer
    - `flaky` boolean
  - `coverage` object: Optional coverage data collected during the test run
    - `format` string, required: Coverage format (e.g. "lcov")
    - `lines` IngestCoverageMetrics, required
      - `covered` integer, required
      - `total` integer, required
    - `branches` IngestCoverageMetrics, required
      - `covered` integer, required
      - `total` integer, required
    - `functions` IngestCoverageMetrics, required
      - `covered` integer, required
      - `total` integer, required
    - `files` object[], required
      - `path` string, required
      - `lines` IngestCoverageMetrics, required
        - `covered` integer, required
        - `total` integer, required
      - `branches` IngestCoverageMetrics, required
        - `covered` integer, required
        - `total` integer, required
      - `functions` IngestCoverageMetrics, required
        - `covered` integer, required
        - `total` integer, required

## Response `202`

Payload accepted for async processing

- object
  - `uploadSessionId` string: ID of the upload session. Use the upload-sessions endpoint to poll for completion

## Other responses

- `400`: Invalid payload (validation failed), or a user API key was used without `projectId`
- `401`: Missing or invalid API key
- `429`: Project upload rate limit exceeded. Retry after the interval in the `Retry-After` header.

---

[API](https://skmtc.dev/gaffer/apis/gaffer-api.md) · [All operations](https://skmtc.dev/gaffer/apis/gaffer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/gaffer/apis/gaffer-api/revisions/d742ab303118?raw)
