> ## Documentation Index
> Fetch the complete documentation index at: https://docs.peepsai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Send lab results

> Send a lab run's pytest JUnit to a project backed by a repository, filed as the repository's CI runs at the commit and branch the lab tested.

Sends one lab run of a pytest suite that never runs in your CI, such as device tests on lab hardware. Peeps files it as a CI run of the project's repository, at the commit and branch you send, and analyzes its failures. An upload of a fix's `peeps/…` branch can also settle that fix's pull request. For the setup and an example workflow, see [Send results from your lab](https://docs.peepsai.com/repository/lab-results.md).

The project must be backed by a repository, and the key must name it under **Only these projects**. A key for all web projects can't send lab results, and gets `404`.

Sending again files a second batch: Peeps doesn't recognize a resend. Lab runs count toward the same minute limits as report files. See [Rate limits](https://docs.peepsai.com/api-reference/introduction.md#rate-limits).

## Errors

Besides `401`, `429`, `503` and `500` from the [errors table](https://docs.peepsai.com/api-reference/introduction.md#errors), sending lab results can return:

| Status | Code | When | What to do |
| - | - | - | - |
| `400` | `invalid_request` | `Content-Length` isn't a number, the body is empty or isn't the size `Content-Length` says, the body isn't JSON, or a field is missing or invalid. The message names the field. | Fix the request and send it again. |
| `404` | `not_found_or_no_access` | No project has that acronym or ID in your organization, or the key doesn't name it under **Only these projects**. | Check the project and the key's projects. |
| `408` | `body_timeout` | The body didn't arrive within 120 seconds. | Send it again. |
| `411` | `length_required` | The request has no `Content-Length` header. | Send the body with its size, as `curl --data-binary` does. |
| `413` | `too_large` | The body is over 50 MiB. | Send a smaller report. |
| `413` | `too_many_tests` | The JUnit reports more than 2,000 tests. | Split the run into several uploads. |
| `422` | `platform_not_supported` | The project isn't backed by a repository. | Send lab results to a project backed by a repository, or send a [report file](https://docs.peepsai.com/api-reference/results/send-a-report-file.md) instead. |
| `422` | `not_connected` | The project is no longer connected to a repository. | Connect the repository again. |
| `422` | `branch_not_followed` | Peeps doesn't follow `gitRef`, or it's a fix's branch and `commitSha` isn't the commit Peeps pushed there. The message names the branches or the commit to send. | Send results for the headline branch or a tracked branch, or for the fix's commit. |
| `422` | `unknown_commit` | `commitSha` isn't a commit on `gitRef`. | Send the full 40-character commit the tests were built from. |
| `422` | `malformed_report` | Peeps couldn't read `junit` as a JUnit report. | Send the XML that pytest's `--junitxml` option wrote. |
| `422` | `test_not_reported` | `gitRef` is a fix's branch, and the JUnit doesn't report the test the fix is for. Nothing is filed. | Run that test. If pytest's rootdir isn't the repository root, send it as `rootDir`. |

```bash curl theme={null}
jq -n --arg sha "$COMMIT_SHA" --arg ref "$BRANCH" --rawfile junit junit.xml \
  '{commitSha: $sha, gitRef: $ref, rootDir: ".",
    context: {cameraModel: "CD62", firmwareVersion: "v2026.10.27.2"},
    junit: $junit}' > lab-results.json

curl --fail-with-body -sS -X POST --data-binary @lab-results.json \
  -H "Authorization: Bearer $PEEPS_API_KEY" \
  -H "Content-Type: application/json" \
  "https://app.peepsai.com/api/v1/projects/LAB/ci-results"
```

```json 201 theme={null}
{
  "batchNumber": 42,
  "filed": 18,
  "unmatched": [
    { "path": "tests/device/test_new_mode.py", "titlePath": "test_night_mode" }
  ]
}
```

```json 201 (fix's branch) theme={null}
{
  "batchNumber": 43,
  "filed": 1,
  "unmatched": [],
  "verification": { "pullRequest": 128, "state": "ready_for_review" }
}
```

```json 422 theme={null}
{
  "error": {
    "code": "unknown_commit",
    "message": "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c is not a commit on main in acme/camera-tests. Send the full 40-character commit the tests were built from."
  }
}
```


## OpenAPI

````yaml api-reference/openapi.json POST /projects/{project}/ci-results
openapi: 3.1.0
info:
  title: Peeps API
  version: '1'
servers:
  - url: https://app.peepsai.com/api/v1
security:
  - apiKey: []
paths:
  /projects/{project}/ci-results:
    post:
      summary: Send lab results
      operationId: sendLabResults
      parameters:
        - name: project
          in: path
          required: true
          description: >-
            The project's acronym, in any case, such as `LAB`, or its ID. The
            project must be backed by a repository, and the key must name it
            under **Only these projects**.
          schema:
            type: string
      requestBody:
        required: true
        description: >-
          The lab run as JSON, up to 50 MiB. Send its size in bytes as
          `Content-Length`; `curl --data-binary @file` sends it for you. The
          whole body must arrive within 120 seconds.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LabResults'
      responses:
        '201':
          description: Peeps filed the run.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FiledLabResults'
        '400':
          description: >-
            `invalid_request`. See [Send lab
            results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          description: >-
            `not_found_or_no_access`. See [Send lab
            results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '408':
          description: >-
            `body_timeout`. See [Send lab
            results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '411':
          description: >-
            `length_required`. See [Send lab
            results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: >-
            `too_large` or `too_many_tests`. See [Send lab
            results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            `platform_not_supported`, `not_connected`, `branch_not_followed`,
            `unknown_commit`, `malformed_report` or `test_not_reported`. See
            [Send lab results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          description: >-
            `busy` or `results_unavailable`. See [Send lab
            results](/api-reference/results/send-lab-results#errors).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            Retry-After:
              description: >-
                With `busy`, the number of seconds to wait before you send the
                run again.
              schema:
                type: integer
components:
  schemas:
    LabResults:
      type: object
      required:
        - commitSha
        - gitRef
        - junit
      additionalProperties: false
      properties:
        commitSha:
          type: string
          pattern: ^[0-9a-fA-F]{40}$
          description: >-
            The full 40-character commit the tests were built from. It must be
            on `gitRef`: its latest commit or an earlier one.
        gitRef:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            The branch the commit is on: the headline branch, a branch listed
            under **Tracked branches**, or the `peeps/…` branch of a fix waiting
            for the lab, at the commit Peeps pushed.
        rootDir:
          type: string
          maxLength: 500
          default: .
          description: >-
            pytest's rootdir, relative to the repository root, with no `.` or
            `..` segment. Leave it out, or send `.`, when pytest runs from the
            repository root.
        jobUrl:
          type:
            - string
            - 'null'
          format: uri
          maxLength: 2000
          description: >-
            A link to the lab run. Peeps shows it as **Lab run:** in the comment
            it leaves on a fix the upload settles.
        context:
          type: object
          additionalProperties: false
          description: >-
            What the lab knows about the run. Peeps shows it first on each
            result, and names the batch after the camera model and firmware. Any
            other field is refused.
          properties:
            cameraModel:
              type: string
              minLength: 1
              maxLength: 200
              description: The camera model, such as `CD62`.
            firmwareVersion:
              type: string
              minLength: 1
              maxLength: 200
              description: The firmware version, such as `v2026.10.27.2`.
            machine:
              type: string
              minLength: 1
              maxLength: 200
              description: The machine the run used.
            serialNumber:
              type: string
              minLength: 1
              maxLength: 200
              description: The device's serial number.
            suite:
              type: string
              minLength: 1
              maxLength: 200
              description: The suite the run belongs to.
        junit:
          type: string
          minLength: 1
          description: The JUnit XML that pytest's `--junitxml` option wrote, as text.
    FiledLabResults:
      type: object
      properties:
        batchNumber:
          type: integer
          description: The number of the batch Peeps filed the runs in.
        filed:
          type: integer
          description: How many tests Peeps filed.
        unmatched:
          type: array
          description: >-
            Tests in the JUnit that the project has no test case for. An upload
            never creates a test case, so these aren't filed.
          items:
            type: object
            properties:
              path:
                type: string
                description: The test's file, relative to the repository root.
              titlePath:
                type: string
                description: The test function.
        verification:
          type: object
          description: >-
            Only for an upload of a fix's `peeps/…` branch: what the upload did
            to the fix's pull request.
          properties:
            pullRequest:
              type: integer
              description: The fix's pull request number.
            state:
              type: string
              enum:
                - ready_for_review
                - closed
                - awaiting
                - already_settled
                - pull_request_not_open
                - pull_request_moved
                - unsettled
              description: >-
                One of:


                - `ready_for_review`: the test passed, and the pull request is
                ready for review.

                - `closed`: the test failed, and Peeps closed the pull request.

                - `awaiting`: every result was skipped, so the pull request
                still waits for the lab.

                - `already_settled`: an earlier upload already settled this fix.
                The results are filed, and the pull request is unchanged.

                - `pull_request_not_open`: the pull request was already closed
                or merged, so Peeps left it alone.

                - `pull_request_moved`: someone pushed to the pull request's
                branch after Peeps, so Peeps left it alone.

                - `unsettled`: Peeps couldn't change the pull request. The
                results are filed, and the next upload tries again.
    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: A code you can act on.
            message:
              type: string
              description: A message you can show.
  responses:
    Unauthenticated:
      description: >-
        `unauthenticated`. See the [errors
        table](/api-reference/introduction#errors).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: >-
        `rate_limited`. See the [errors
        table](/api-reference/introduction#errors).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
      headers:
        Retry-After:
          description: The number of seconds to wait before you send the request again.
          schema:
            type: integer
    Internal:
      description: >-
        `internal`. It's safe to send the request again. See the [errors
        table](/api-reference/introduction#errors).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: '`Bearer` followed by your [API key](/api-reference/api-keys).'

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.