> ## 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 deployments

> Show the resources your plugin deployed, and links to them, on an Entire Change.

A plugin registered with `deployments:write` can report the resources it deployed for a commit, such as a preview site, with their status and links. Entire attaches them to the commit's Change, whose detail lists them with a ready count, such as 3 of 4 ready. The Deployments section on entire.com is still being built. The Deployments API only reports deployments: it doesn't create them, and it never blocks a merge.

> [!NOTE]
>
> Deployments are on Entire's v0 API. Field names may change before the contract is final.

## What you need

* **`deployments: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 grants nothing else. `checks:write`, `contents` permissions and user tokens don't grant it.
* **The project's acceptance.** Adding the permission to your plugin doesn't grant it to existing installations. It stays pending until a project manager accepts it. Until then, writes for that project's repositories answer `403`, so keep reporting there through check runs or [trail comments](https://docs.entire.com/guides/api/plugins/trail-comments.md).

Send every call below to the repository's [`api_base_url`](https://docs.entire.com/guides/api/regions-and-routing), under `/api/v0` rather than `/api/v1`, with the installation's [access token](https://docs.entire.com/guides/api/plugins/act-for-an-installation.md#mint-an-access-token) as bearer token. Use the public `repo_…` repository ID and the full 40- or 64-character commit SHA. The [Go SDK](#with-the-go-sdk) does the minting and routing for you.

## Report a resource

```http theme={null}
POST /api/v0/repos/{repo_id}/commits/{sha}/deployments
Authorization: Bearer {access token}
Content-Type: application/json

{
  "branch": "feature-x",
  "deployments": [{
    "key": "preview-site",
    "name": "Preview site",
    "status": "completed",
    "conclusion": "success",
    "environment_url": "https://preview.example.com",
    "external_updated_at": "2026-10-24T09:12:00Z"
  }]
}
```

Set `external_updated_at` to when the state changed on your side, in UTC, not to when you send the request. Keep each resource's `key` the same across updates and across commits.

| Field | Required | Notes |
| :- | :- | :- |
| `branch` | Yes | The branch name, without `refs/heads/`. |
| `deployments` | Yes | 1 to 1,000 resources, each with a different `key`. The body is limited to 1 MiB. |
| `key` | Yes | Your stable ID for the resource, up to 255 characters. Keys are per plugin: another plugin's `preview-site` is a different resource. |
| `name` | Yes | Shown on the Change, up to 255 characters. |
| `status` | Yes | `queued`, `in_progress`, `completed` or `inactive`. See [Keep it current](#keep-it-current). |
| `conclusion` | With `completed` | `success`, `failure`, `cancelled` or `skipped`. Omit it for every other status. |
| `external_updated_at` | Yes | UTC, no more than 60 seconds in the future. |
| `environment` | No | Defaults to `preview`, the only environment Entire documents in v0. Other values, up to 64 characters, are stored and returned but treated the same way. |
| `environment_url` | No | The deployed resource, as an `https` URL without credentials, up to 2,048 bytes. |
| `logs_url` | No | The deployment's logs, with the same rules. |
| `type` | No | A free-form kind, such as `site` or `function`, up to 64 characters. |
| `group` | No | `{"key": "...", "name": "..."}`, both required, to show related resources under one heading. |
| `started_at`, `completed_at` | No | UTC. `completed_at` can't be before `started_at`. |
| `deadline_at` | No | UTC, at most 24 hours ahead. A queued or in-progress resource past its deadline is marked stale. |

The response has one result per resource, in request order:

```json theme={null}
{"results": [{"deployment": {"id": "...", "key": "preview-site", "status": "completed", "conclusion": "success"}, "outcome": "created"}]}
```

`outcome` is `created`, `updated`, `unchanged` or `ignored_stale`. `deployment` is the stored state, whether or not your write applied.

## Keep it current

* Report `queued`, then `in_progress`, then `completed` with a `conclusion`.
* Report `inactive` when you remove the deployed resource, such as a torn-down preview, and omit `conclusion`, so its links stop being offered.
* Retries are safe. Entire orders writes by `external_updated_at`, so an older report never overwrites a newer one. At an equal time, a write never moves a resource back to an earlier status, so a late retry can't revive a torn-down preview. Reuse the same `external_updated_at` when you retry.
* Report each commit you deploy. A Change shows only the deployments on its head commit, never an earlier commit's.

Deployments never gate a merge. If you use [check runs](https://docs.entire.com/guides/api/plugins/go-sdk.md#report-check-runs) for merge gating, keep publishing them separately. Using the same key for a check and a deployment is a convention, not a link Entire makes.

## Read your reports

`GET` on the same URL returns your plugin's current report for each key on that commit, ordered by key. It returns only your plugin's deployments:

```json theme={null}
{"deployments": [{"id": "...", "plugin": "acme-previews", "key": "preview-site", "name": "Preview site", "environment": "preview", "status": "completed", "conclusion": "success", ...}], "next_cursor": "..."}
```

Pass `?per_page=` (1 to 100, default 30). While the response has a `next_cursor`, pass it back unchanged as `?cursor=` for the next page.

## Errors

Errors are `application/problem+json`. Branch on `code`, not on the status.

| Status | `code` | Meaning | Retry? |
| :- | :- | :- | :- |
| `401` | `unauthenticated` | The token is missing or invalid. | After minting a new token. |
| `403` | `forbidden` | The token lacks `deployments:write`, or the project hasn't accepted it. | No. |
| `403` | `repo_not_covered` | The installation doesn't cover this repository. | No. |
| `409` | `wrong_cell` | The repository moved to another region. | Yes, at its new `api_base_url`. |
| `409` | `deployment_owner_conflict` | Another live installation of your plugin owns a key in the batch. | No. |
| `413` | `request_too_large` | The body is over 1 MiB. | Split the batch. |
| `422` | `validation_error` | The body, SHA, `per_page` or `cursor` is invalid. `detail` says which. | No. |
| `503` | `service_unavailable` | Deployment storage is unreachable. | Yes, with backoff. |

## With the Go SDK

Add `Deployments: plugin.Write` to `Config.Permissions`, matching your registration. `inst.Deployments` then reports and reads deployments:

```go theme={null}
func reportPreview(ctx context.Context, inst *plugin.Installation, repoID, sha, branch, url string, deployedAt time.Time) error {
	_, err := inst.Deployments.Upsert(ctx, repoID, sha, plugin.DeploymentsRequest{
		Branch: branch,
		Deployments: []plugin.Deployment{{
			Key:               "preview-site",
			Name:              "Preview site",
			Status:            plugin.Completed,
			Conclusion:        plugin.Success,
			EnvironmentURL:    url,
			ExternalUpdatedAt: deployedAt,
		}},
	})
	return err
}
```

The SDK validates a write as Entire does before sending it, sends it to the repository's region, retries it with the same body, and follows a repository that moved to another region. Report a removed resource with `Status: plugin.Inactive`.

`inst.Deployments.List` returns one page of your current reports, and `inst.Deployments.All` iterates over all of them. A refusal is an `*entire.HTTPError`: check it with `entire.HasCode`, for example `entire.HasCode(err, entire.CodeForbidden)` for a project that hasn't accepted `deployments:write` yet.
