# Annotations

> The Annotation object, and the endpoints to create, list, retrieve, update and delete annotations.

An annotation records a deploy, a change or a comment next to your telemetry. To mark deploys from CI, start from the [Annotations guide](https://telemetrymachine.com/docs/annotations).

- [`POST /`](https://telemetrymachine.com/docs/api/annotations#create-an-annotation) Create
- [`GET /`](https://telemetrymachine.com/docs/api/annotations#list-annotations) List
- [`GET /{public_id}`](https://telemetrymachine.com/docs/api/annotations#retrieve-an-annotation) Retrieve
- [`PATCH /{public_id}`](https://telemetrymachine.com/docs/api/annotations#update-an-annotation) Update
- [`DELETE /{public_id}`](https://telemetrymachine.com/docs/api/annotations#delete-an-annotation) Delete

Anyone in the organization may read annotations. Those on a restricted dataset are visible only to people who can read that dataset. Each endpoint says who may write.

## The Annotation object

Badges say what a create request needs: _required_, _optional_, or _server_ when Telemetry Machine sets it.

**Attributes**

- `public_id` string _server_

  `ann_…`

- `type` enum _required_

  Why the data might look different (refer to [Types](https://telemetrymachine.com/docs/annotations#types)).

  One of `deploy`, `change`, `comment`

- `message` string _required_

  What happened, such as `checkout 1.42.0`.

  Rules: up to 4,096 characters.

- `started_at` timestamp _optional_

  When it happened, or when it started, such as `2026-10-23T14:02:00Z`. Defaults to now. `time` is its older name, still accepted.

  Rules: RFC 3339 with a zone.

- `ended_at` timestamp, nullable _optional_

  When it ended, for an annotation that covers a window, such as a load test. Null for a moment (refer to [Moments and windows](https://telemetrymachine.com/docs/api/annotations#moments-and-windows)).

  Rules: RFC 3339 with a zone, not before `started_at`.

- `key` string, nullable _optional_

  Your own id for it, unique in the organization. It makes a create safe to retry (refer to [Keys](https://telemetrymachine.com/docs/api/annotations#keys)).

  Rules: up to 255 characters, not blank.

- `dataset` string, nullable _optional_

  The dataset it concerns, by name or public id. Null for the whole organization.

  Rules: up to 255 characters; a dataset that you can read.

- `attributes` map _optional_

  What it concerns, with OpenTelemetry names, such as `service.name`. A deploy's are checked (refer to [Deploy attributes](https://telemetrymachine.com/docs/api/annotations#deploy-attributes)).

  Rules: up to 64 names of 1 to 255 characters. Each value is a string of up to 1,024 characters, a number or a boolean.

- `created_by` object, nullable _server_

  The person who posted it.

- `created_by_api_key` object, nullable _server_

  The ingest key that posted it.

- `created_by_ci` object, nullable _server_

  The CI job that posted it with its OIDC token (refer to [CI trust rules](https://telemetrymachine.com/docs/api/ci-trust-rules)).
  - `provider` enum

    `github` or `gitlab`.

  - `repository`, `repository_id` string

    The repository, such as `acme/checkout`, and the provider's id for it.

  - `ref` string

    The job's ref, such as `refs/heads/main`.

  - `environment` string, nullable

    The job's deployment environment, such as `production`.

  - `workflow` string, nullable

    The workflow file, such as `acme/checkout/.github/workflows/publish.yml@refs/heads/main`.

  - `run_id`, `run_url` string

    The run, and a link to its log.

  - `rule` string

    The trust rule that let the job in, `ctr_…`.

- `created_at` timestamp _server_

  When it was posted.

- `updated_at` timestamp _server_

  When it last changed, such as when it ended.

```json status="200" title="The Annotation object"
{
  "public_id": "ann_01m5x9q2r7t4k8w3e6y1p0z5ab",
  "type": "deploy",
  "message": "checkout 1.42.0",
  "started_at": "2026-10-26T13:50:04.000Z",
  "ended_at": null,
  "key": "checkout@1.42.0@production",
  "dataset": null,
  "attributes": {
    "service.name": "checkout",
    "service.version": "1.42.0",
    "deployment.environment.name": "production",
    "vcs.ref.head.revision": "0b7c0a8e5d1f4c2b9a6e3d7f1c8b4a2e9d5f6c31",
    "url.full": "https://github.com/acme/checkout/actions/runs/123"
  },
  "created_by": null,
  "created_by_api_key": { "public_id": "apk_01m5…", "name": "github actions" },
  "created_by_ci": null,
  "created_at": "2026-10-26T13:50:05.000Z",
  "updated_at": "2026-10-26T13:50:05.000Z"
}
```

Responses carry it in the API's envelope, as `data` (refer to [Responses](https://telemetrymachine.com/docs/api#responses)).

## Create an annotation

`POST /api/organizations/{org}/annotations`

- Auth: A token with `annotations:write`, an ingest key with annotate on, or a CI job's OIDC token that a [trust rule](https://telemetrymachine.com/docs/api/ci-trust-rules) lets in
- Returns: [The Annotation object](https://telemetrymachine.com/docs/api/annotations#the-annotation-object)

Takes the fields of [the Annotation object](https://telemetrymachine.com/docs/api/annotations#the-annotation-object) that are marked required or optional. With a `key` that an annotation already has, it returns that annotation instead of adding one (refer to [Keys](https://telemetrymachine.com/docs/api/annotations#keys)).

```sh tab="curl" title="POST /annotations"
curl -X POST https://telemetrymachine.com/api/organizations/<your-org>/annotations \
  -H "X-API-Key: $TM_INGEST_KEY" -H 'Content-Type: application/json' \
  -d '{
    "type": "deploy",
    "message": "checkout 1.42.0",
    "key": "checkout@1.42.0@production",
    "attributes": {
      "service.name": "checkout",
      "service.version": "1.42.0",
      "deployment.environment.name": "production",
      "vcs.ref.head.revision": "0b7c0a8e5d1f4c2b9a6e3d7f1c8b4a2e9d5f6c31"
    }
  }'
```

```http tab="HTTP" title="POST /annotations"
POST /api/organizations/<your-org>/annotations
X-API-Key: KEY
Content-Type: application/json

{
  "type": "deploy",
  "message": "checkout 1.42.0",
  "key": "checkout@1.42.0@production",
  "attributes": {
    "service.name": "checkout",
    "service.version": "1.42.0",
    "deployment.environment.name": "production",
    "vcs.ref.head.revision": "0b7c0a8e5d1f4c2b9a6e3d7f1c8b4a2e9d5f6c31"
  }
}
```

```json status="200" title="Response"
{
  "status": 200,
  "data": {
    "public_id": "ann_01m5x9q2r7t4k8w3e6y1p0z5ab",
    "type": "deploy",
    "message": "checkout 1.42.0",
    "started_at": "2026-10-26T13:50:04.000Z",
    "ended_at": null,
    "key": "checkout@1.42.0@production"
  }
}
```

The response is the whole annotation; this one is cut short.

## List annotations

`GET /api/organizations/{org}/annotations`

- Auth: Any member, with a token or from the console
- Returns: `annotations`, newest first, and `total`, `limit` and `offset` for paging

**Query parameters**

- `from` timestamp _optional_

  Only annotations at or after it. A window that began earlier and ends after it, such as a load test, is included.

- `to` timestamp _optional_

  Only annotations that start at or before it.

- `type` enum _optional_

  Only annotations of this type.

- `dataset` string _optional_

  Only annotations about this dataset, and those for the whole organization.

- `limit` integer _optional_

  How many to return.

  Rules: 1 to 1,000; defaults to 100.

- `offset` integer _optional_

  How many to skip, for paging; defaults to 0.

## Retrieve an annotation

`GET /api/organizations/{org}/annotations/{public_id}`

- Auth: Any member who can read its dataset
- Returns: [The Annotation object](https://telemetrymachine.com/docs/api/annotations#the-annotation-object)

## Update an annotation

`PATCH /api/organizations/{org}/annotations/{public_id}`

- Auth: A token with `annotations:write`, or the ingest key that created it. A CI job's token can't update.
- Returns: [The Annotation object](https://telemetrymachine.com/docs/api/annotations#the-annotation-object)

Updates the fields that you send, and leaves the rest. Setting `ended_at` turns a moment into a window, such as a load test that you mark when it finishes.

**Body parameters**

- `ended_at` timestamp, nullable _optional_

  When it ended. Null makes it a moment again.

- `message` string _optional_

  What happened.

- `started_at` timestamp _optional_

  When it happened, or when it started.

- `dataset` string, nullable _optional_

  A dataset, or null to make it cover the whole organization.

- `type`, `attributes` _optional_

  As on [the Annotation object](https://telemetrymachine.com/docs/api/annotations#the-annotation-object). `attributes` replaces them all.

```sh tab="curl" title="PATCH /annotations/{public_id}"
curl -X PATCH https://telemetrymachine.com/api/organizations/<your-org>/annotations/ann_01m5y2k8r4t6w9e3q7p1z0x5cd \
  -H "Authorization: Bearer $TM_TOKEN" -H 'Content-Type: application/json' \
  -d '{ "ended_at": "2026-10-26T15:00:00Z" }'
```

```http tab="HTTP" title="PATCH /annotations/{public_id}"
PATCH /api/organizations/<your-org>/annotations/ann_01m5y2k8r4t6w9e3q7p1z0x5cd
Authorization: Bearer TOKEN
Content-Type: application/json

{ "ended_at": "2026-10-26T15:00:00Z" }
```

```json status="200" title="Response"
{
  "status": 200,
  "data": {
    "public_id": "ann_01m5y2k8r4t6w9e3q7p1z0x5cd",
    "type": "change",
    "message": "load test of checkout",
    "started_at": "2026-10-26T14:00:00.000Z",
    "ended_at": "2026-10-26T15:00:00.000Z"
  }
}
```

## Delete an annotation

`DELETE /api/organizations/{org}/annotations/{public_id}`

- Auth: A token with `annotations:write`. Ingest keys and CI jobs can't delete.
- Returns: `{ "deleted": true }`

## Moments and windows

An annotation is a moment at `started_at`. With `ended_at`, it covers a window, such as a load test or a comment on a stretch of a chart. Send `ended_at` with the annotation, or set it later with [Update](https://telemetrymachine.com/docs/api/annotations#update-an-annotation). Nothing ends an annotation by itself.

## Keys

A key is your own id for an annotation, unique in the organization. It makes a create safe to retry, so CI can send the same call again after a timeout.

- Posting a key again returns the existing annotation when every field that you send matches it: a retry. Fields that you leave out aren't compared, so a retry needn't send its time again.
- Posting a key again with a field that differs is refused with 409 Conflict, so two different things never share a key silently.
- A retry is answered even when the organization is at its [daily limit](https://telemetrymachine.com/docs/api/annotations#limits).
- A key that another poster holds is a conflict for a CI job: a job retries only the keys that its own repository posted.

A key names everything that makes it unique. Something from another system is `<source>:<id>`, such as `launchdarkly:new-checkout:42` for a flag's 42nd version. A deploy is its service, version and environment, such as `checkout@1.42.0@production`, because the same version often goes to staging and then to production.

## Deploy attributes

A `deploy` must name a commit or a service. These checks run when it is created, and when an update changes its type or attributes. Other names aren't checked, so you can send your own.

**Deploy attributes**

- `service.name` string

  The service that it deploys.

  Rules: required unless the deploy names a commit; not blank.

- `service.version` string

  The version that it ships, as the service reports it.

- `deployment.environment.name` string

  The environment, such as `production`.

  Rules: not blank.

- `vcs.ref.head.revision` string

  The commit that it ships.

  Rules: 7 to 40 hex characters, such as `$GITHUB_SHA`; required unless the deploy names a `service.name`.

- `url.full` string

  A link to the CI run.

## Limits

An organization can post 1,000 annotations in 24 hours. More get 429: send events at that volume to an [events dataset](https://telemetrymachine.com/docs/json-events) instead. A retry of a key is still answered.

## Errors

| Status | When                                                                                                                               | What to do                                                                                                       |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| 400    | `type` is not one of the types                                                                                                     | Use one of the types that the error names.                                                                       |
| 400    | `attributes` breaks a limit: more than 64 names, a name over 255 characters, or a value that isn't a string, a number or a boolean | Fix the attribute that the error names.                                                                          |
| 400    | A `deploy` names neither `vcs.ref.head.revision` nor `service.name`, or its commit isn't 7 to 40 hex characters                    | Send the commit as CI has it, such as `$GITHUB_SHA`.                                                             |
| 400    | `ended_at` is before `started_at`, or `started_at` and `time` differ                                                               | Send one start, and an end after it.                                                                             |
| 400    | `dataset` names no dataset that you can read                                                                                       | Use the dataset's name or its public id.                                                                         |
| 401    | A CI job's token is invalid: its signature, issuer or audience is wrong, or it expired                                             | Ask the provider for a new token, addressed to `telemetrymachine.com`.                                           |
| 403    | The token lacks `annotations:write`, or the ingest key doesn't have annotate on                                                    | Use a token with the scope, or an ingest key with annotate on.                                                   |
| 403    | No trust rule lets the CI job in, or a CI job's token tried to update                                                              | Add a [trust rule](https://telemetrymachine.com/docs/api/ci-trust-rules) for the repository, or check the ref and environment that it names. |
| 404    | The organization has no annotation with this `public_id`, or you can't read its dataset                                            | Check the id.                                                                                                    |
| 409    | The key exists with a different body                                                                                               | The key names something else. Add what makes it unique, such as the environment.                                 |
| 429    | The organization posted 1,000 annotations in the last 24 hours                                                                     | Send events at this volume to an [events dataset](https://telemetrymachine.com/docs/json-events) instead.                                    |

---

All pages: https://telemetrymachine.com/docs/llms.txt
