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

# Report findings on trails

> Publish review findings on a trail's code, and resolve them as they're fixed.

A plugin registered with `findings:write` can publish findings on a trail: comments tied to a line, a range, a file or the whole change, each with an optional severity and confidence. For example, a security scanner starts a review when a [trail opens or updates](https://docs.entire.com/guides/api/plugins/webhooks.md#trail-events), publishes what it found, and resolves each finding once a later push fixes it. Findings appear on the trail's diff next to the code they're about.

> [!NOTE]
>
> Trail findings are experimental. Field names may change before the contract is final.

## What you need

* **`findings:write`**, on your plugin's installation token. Ask Entire to add it when you [register your plugin](https://docs.entire.com/guides/api/plugins/register.md). It also lets you read findings, but grants no repository read or push access.
* **`contents:read`**, to read the code you're reviewing and receive [trail events](https://docs.entire.com/guides/api/plugins/webhooks.md#what-you-need). The token you write findings with doesn't need it.
* **An access token scoped to the repository** for writing findings. [Mint it](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#mint-an-access-token) with this body:

  ```json theme={null}
  {"repository_id": "repo_01M3...", "permissions": {"findings": "write"}}
  ```

Send every call below to the repository's [`api_base_url`](https://docs.entire.com/guides/api/regions-and-routing), under `/api/v1`, with the access token as bearer token. Trails must be enabled on the repository.

## Start a review

Findings belong to a review. [Start one](https://docs.entire.com/api-reference/trails/trail-reviews/start-a-code-review-repo_id-addressing.md) for the commit you analyzed:

```http theme={null}
POST /api/v1/repos/{repo_id}/trails/{number}/reviews
Content-Type: application/json
Idempotency-Key: scan-4f2a9c1e

{"head_sha": "4f2a9c1e..."}
```

Every body field is optional. `head_sha` and `base_sha` are full 40-character SHAs; the trail's current head and base fill whichever you omit. Pass the `head_sha` you analyzed so your findings attach to that code, even if someone pushes while you work. A `201` returns the review:

```json theme={null}
{"review_id": "01K...", "trail_id": "...", "repo_id": "repo_01M3...", "code_version_id": "...", "base_sha": "...", "head_sha": "4f2a9c1e...", "limits": {"max_comments_per_batch": 100}, ...}
```

Store `review_id`. Only your installation can publish to that review or change its findings.

## Publish findings

[Publish your findings](https://docs.entire.com/api-reference/trails/trail-reviews/atomically-publish-review-comments-and-summary-repo_id-addressing.md) on the review, with an optional summary:

```http theme={null}
POST /api/v1/repos/{repo_id}/trails/{number}/reviews/{review_id}/comments
Content-Type: application/json
Idempotency-Key: scan-4f2a9c1e-publish

{
  "summary": "1 high-severity issue in 12 files scanned.",
  "comments": [{
    "client_id": "sql-injection:api/users.go:42",
    "body": "User input reaches this query unescaped. Use a parameterized query.",
    "severity": "high",
    "confidence": 0.9,
    "location": {
      "granularity": "line",
      "file_path": "api/users.go",
      "start_line": 42,
      "selected_text": "db.Query(\"SELECT * FROM users WHERE id = \" + id)"
    }
  }]
}
```

The whole request applies or none of it does. It also marks the review complete, so publish everything you found for that commit in one request. To report no findings, send an empty `comments` array, with a summary if you like.

| Field | Required | Notes |
| :- | :- | :- |
| `comments` | No | Up to 100 findings. |
| `summary` | No | Your review's overall summary, in Markdown. `null` clears it. |
| `client_id` | Yes | Your stable ID for the finding, unique in the request, up to 255 characters. |
| `body` | No | The finding, in Markdown, up to 65,536 characters. |
| `severity` | No | `high`, `medium` or `low`. |
| `confidence` | No | A number from 0 to 1. |
| `status` | No | `open` (the default), `resolved` or `dismissed`. |
| `status_reason` | No | Why the finding was resolved or dismissed, up to 4,096 characters. |
| `location` | Yes | Where the finding applies. See below. |
| `suggested_change` | No | A proposed fix. `change_type` is `unified_diff`, `file_replacement`, `json_patch` or `manual_instruction`. |

`location.granularity` decides what else `location` needs:

| `granularity` | Also requires |
| :- | :- |
| `line` | `file_path`, `start_line` (from 1) and `selected_text`, the text on that line. |
| `range` | The same, plus `end_line`, no less than `start_line`. |
| `file` | `file_path`. |
| `whole_change` | Nothing. |

`start_column`, `end_column`, `nearby_text` and `language` are optional. `selected_text` and `nearby_text` help keep a finding attached to its code as the trail changes.

The response has the completed review and one result per finding, in request order:

```json theme={null}
{"review": {"id": "01K...", "summary": "...", "completed_at": "2026-11-09T10:04:00Z", ...}, "results": [{"client_id": "sql-injection:api/users.go:42", "status": "created", "comment": {"id": "01K...", "status": "open", ...}}]}
```

Store each `comment.id` with its `client_id`. You need the ID to resolve the finding later.

## Retry safely

Send an `Idempotency-Key` header with both requests. Retrying with the same key and body returns the first response rather than starting a second review or publishing twice.

`client_id` protects each finding as well. Publishing a `client_id` the review already has, with the same payload, returns it with `status` `existing` instead of creating a duplicate. The same `client_id` with a different payload fails the request.

## Keep findings current

[Update a finding](https://docs.entire.com/api-reference/trails/trail-reviews/update-a-review-comment-repo_id-addressing.md) when its state changes, such as when a later push fixes it:

```http theme={null}
PATCH /api/v1/repos/{repo_id}/trails/{number}/reviews/{review_id}/comments/{comment_id}
Content-Type: application/json

{"status": "resolved", "status_reason": "Fixed in 9b7d3e0: the query is now parameterized."}
```

Send only the fields you're changing: `body`, `severity`, `confidence`, `status` or `status_reason`. Moving a finding to `resolved` or `dismissed` requires a non-empty `status_reason`. Set `status` back to `open` to reopen it.

When someone pushes new code, start a new review for the new head rather than adding to an earlier one. A finding tied to code that has since changed can become stale. It keeps its status, but lists of current findings leave it out.

## Read findings

[List the trail's findings](https://docs.entire.com/api-reference/trails/trail-reviews/list-all-review-comments-for-a-trail-repo_id-addressing.md) with `GET /api/v1/repos/{repo_id}/trails/{number}/reviews/comments`. The list includes other reviewers' findings, so match on your stored `review_id` to find yours.

By default it returns current findings that aren't dismissed. Filter with `?status=`, `?severity=` and `?stale=current|stale|any`. While the response has a `next_cursor`, pass it back as `?cursor=` for the next page.

## Findings and merge gates

Findings your plugin publishes don't count toward a trail's [Agent review gate](https://docs.entire.com/guides/trails/gates.md#agent-review), whatever their severity, and never block a merge. To block a merge on your results, also [report checks](https://docs.entire.com/guides/api/plugins/report-checks.md), which needs `checks:write`. Your plugin's check gate blocks merging by default; see [the plugin gate rules](https://docs.entire.com/guides/api/plugins/go-sdk.md#your-plugins-gate) and [Review and require checks](https://docs.entire.com/guides/ci/checks.md).

## What findings:write doesn't cover

* Changing findings on a review another installation or a person started. Those return `404`.
* Approving a trail, or posting [trail comments](https://docs.entire.com/guides/api/plugins/trail-comments.md), which need other permissions.

## Errors

Errors are `application/problem+json`. For errors when minting the token, see [Access repositories](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#errors).

| Status and detail | Meaning |
| :- | :- |
| `403 no publish access to repo` | Starting a review or publishing findings without `findings:write` on this repository. The same answer covers a repository that doesn't exist. |
| `403 no findings write access to repo` | Updating a finding without `findings:write`. |
| `404 repository not found` | Trails aren't enabled on the repository. |
| `404 Review not found` | The review doesn't belong to this trail, or another installation or a person started it. |
| `404 Review comment not found` | The finding doesn't exist on this review, or it's on a review you didn't start. |
| `409 review comments batch failed atomically` | A finding couldn't be stored, such as a reused `client_id` with a different payload. Nothing in the request was saved. |
| `409 Idempotency-Key already used with a different request body` | Use a new key for a different request. |
| `409 request in flight, retry shortly` | A request with the same `Idempotency-Key` is still running. Retry it unchanged. |
| `409 Change comparison moved while starting the review; retry` | Someone pushed while you started the review. Retry. |
| `400` | A field is invalid or a `location` is missing a required field. The `detail` names which. |
