# Coverage Matrix API

> Retrieve the latest test result per test case × environment for a project over the integrator API.

The coverage matrix endpoint returns the **latest result status** for every test case in a project, broken down by environment. Each row represents a test case; each cell within a row represents the most recent recorded result for that case in one of the project's environments.

The endpoint is **read-only** — it emits no audit events and makes no state changes.

All requests require an authenticated session (API token or cookie) with at least `viewer` membership in the project's organization. A `projectId` that belongs to a different organization returns `404 not_found`.

## Get coverage matrix

`GET /api/v1/projects/{projectId}/coverage-matrix`

`{projectId}` is the project **code** (for example `ACME`).

### Query parameters

| Parameter   | Default | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page`      | `1`     | Page number (1-based)                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `pageSize`  | `50`    | Rows per page (min `1`, max `200`)                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `suiteUlid` | —       | Optional comma-separated list of suite ULIDs. When present, only test cases belonging to any of the listed suites are returned (OR semantics). A single ULID behaves identically to the previous behavior. Unknown or cross-project ULIDs in the list are silently ignored; if all provided ULIDs are unknown the result is an empty page with `total = 0`. A malformed ULID (not a valid 26-character Crockford base32) in the list returns `422 validation_failed`. |
| `status`    | —       | Optional comma-separated filter on derived latest status. Accepted values: `passed`, `failed`, `blocked`, `skipped`, `untested`, `none`. `none` matches rows that have at least one empty cell (a case never run in any environment). Multiple values are OR-combined. Unknown tokens return `422 validation_failed`.                                                                                                                                                 |
| `q`         | —       | Optional text search query. When non-empty (after trimming), restricts rows to cases whose text fields (selected by `searchBy`) contain `q` as a case-insensitive substring. `%` and `_` in `q` match literally. An empty or whitespace-only value is treated as absent (no text filter).                                                                                                                                                                             |
| `searchBy`  | `all`   | Controls which field is searched when `q` is non-empty. Accepted values: `all` (default), `title`, `id`. See the [searchBy values](#searchby-values) table. An unknown value returns `422 validation_failed`.                                                                                                                                                                                                                                                         |

### Response

`200` with the following shape:

```json
{
  "columns": [ ... ],
  "rows": {
    "items": [ ... ],
    "page": 1,
    "pageSize": 50,
    "total": 120
  }
}
```

#### `columns`

An array of environment objects, one per live environment in the project, in creation order. These are the matrix column headers.

| Field           | Type          | Notes                                       |
| --------------- | ------------- | ------------------------------------------- |
| `environmentId` | string (ULID) | The environment's ULID                      |
| `name`          | string        | Display name (for example `Staging`)        |
| `slug`          | string        | URL-safe identifier (for example `staging`) |

Soft-deleted environments are never included.

#### `rows.items`

A paginated array of test-case rows. Each row has the following fields:

| Field        | Type           | Notes                                                                      |
| ------------ | -------------- | -------------------------------------------------------------------------- |
| `testCaseId` | string (ULID)  | The test case's ULID                                                       |
| `caseNumber` | number         | Human-readable case number within the project                              |
| `title`      | string         | Test case title                                                            |
| `suite`      | object \| null | The suite this case belongs to (`{ ulid, name }`), or `null` if unassigned |
| `cells`      | array          | One cell per column, in the same order as `columns`                        |

Each entry in `cells`:

| Field           | Type                  | Notes                                                                                 |
| --------------- | --------------------- | ------------------------------------------------------------------------------------- |
| `environmentId` | string (ULID)         | Matches the corresponding `columns[i].environmentId`                                  |
| `status`        | string \| null        | Latest result status for this case in this environment, or `null` if no result exists |
| `executedAt`    | number \| null        | Unix epoch milliseconds of the most recent result, or `null`                          |
| `runUlid`       | string (ULID) \| null | The run that produced the latest result, or `null`                                    |

A `null` status means the test case has never been run in that environment — it is an **empty cell**, distinct from a result with status `untested`. `cells.length` always equals `columns.length`.

### Status values

| Value      | Meaning                                                      |
| ---------- | ------------------------------------------------------------ |
| `passed`   | The latest result is passing                                 |
| `failed`   | The latest result is failing                                 |
| `blocked`  | The latest result is blocked                                 |
| `skipped`  | The latest result was skipped                                |
| `untested` | The case was explicitly marked untested in the latest result |
| `null`     | No result recorded in this environment (empty cell)          |

### searchBy values

| Value           | Columns matched                                                |
| --------------- | -------------------------------------------------------------- |
| `all` (default) | Case `title` **or** display ID (`#<caseNumber>`) — OR-combined |
| `title`         | Case `title` only                                              |
| `id`            | Display ID only — the `#<caseNumber>` format shown in the UI   |

When `q` is absent or empty, `searchBy` has no effect.

`total` in the response always reflects the count of rows after **all** active filters (suite + status + text) are applied.

### Tiebreaking

When two results for the same `(test_case, environment)` pair share the same `executedAt` timestamp, the result with the greater internal ID wins.

Results from runs with **no environment assigned** (`environment_id IS NULL`) are never included in any cell.

### Errors

| Status | Code                | Condition                                                                                                |
| ------ | ------------------- | -------------------------------------------------------------------------------------------------------- |
| `404`  | `not_found`         | `projectId` does not exist or belongs to a different organization                                        |
| `422`  | `validation_failed` | `pageSize` out of range, unknown `status` token, malformed ULID in `suiteUlid`, unknown `searchBy` value |

### Examples

Filter by two suites and search by title:

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/coverage-matrix?suiteUlid=01JAAAA...,01JBBBB...&q=login&searchBy=title"
```

Filter by status with pagination:

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/coverage-matrix?pageSize=20&status=failed,none"
```

Response excerpt:

```json
{
  "columns": [
    { "environmentId": "01JAAAA...", "name": "Staging", "slug": "staging" },
    { "environmentId": "01JBBBB...", "name": "Production", "slug": "production" }
  ],
  "rows": {
    "items": [
      {
        "testCaseId": "01JCCCC...",
        "caseNumber": 1,
        "title": "Login with valid credentials",
        "suite": { "ulid": "01JDDDD...", "name": "Authentication" },
        "cells": [
          {
            "environmentId": "01JAAAA...",
            "status": "failed",
            "executedAt": 1718700000000,
            "runUlid": "01JEEEE..."
          },
          { "environmentId": "01JBBBB...", "status": null, "executedAt": null, "runUlid": null }
        ]
      }
    ],
    "page": 1,
    "pageSize": 20,
    "total": 3
  }
}
```

## Related pages

- [Environments API](https://docs.probara.net/en/developers/api/environments/) — list the environments that appear as matrix columns.
- [Coverage matrix guide](https://docs.probara.net/en/guides/coverage-matrix/) — user-facing explanation of the matrix screen.
