---
title: "Annotate a batch of project spans"
method: POST
path: "/v2/spans/annotate"
tags: ["Spans"]
---

# Annotate a batch of project spans

`POST /v2/spans/annotate`

Write human annotations to a batch of spans in a project.

**Idempotency**: Writes use upsert semantics — submitting the same annotation
config name for the same span overwrites the previous value. Retrying on
network failure will not create duplicates.

**202 Accepted**: The annotations have been accepted and will be written.
Visibility in read queries may lag by a short interval.

**Partial failure**: Writes are grouped by calendar day and processed
sequentially. A non-2xx response means the request failed during the write
phase — annotations for earlier calendar-day buckets may already be saved
while later ones are not. It is safe to retry the full request;
re-submitting a record that was already saved will overwrite it with the
same value (no duplicates).

**Payload Requirements**
- `project_id` is required and must identify a project the caller has span annotation access to.
- `annotations` is a list of per-span annotation inputs. Each entry identifies
  one span by its `record_id` and provides one or more annotation values.
- Each `record_id` must be unique within the request (duplicates return 400).
- Each record's `values` list must not contain duplicate annotation config names (returns 400).
- `start_time` / `end_time` constrain the time range for span lookup.
  If omitted, `start_time` defaults to 31 days ago and `end_time` to now.
  Both `start_time` and `end_time` may not be in the future. The window may
  not exceed 31 days. If ANY span ID cannot be located within the given
  range, the entire request is rejected with 404 and no annotations are
  written (all-or-nothing pre-validation). Only after all spans are
  confirmed does the write phase begin.
- Annotation names must match existing annotation configs in the project's space.
- Up to 1000 span records may be annotated per request.

**Valid example**
```json
{
  "project_id": "proj_abc123",
  "annotations": [
    {"record_id": "span_abc", "values": [{"name": "relevance", "label": "good", "score": 1.0}]}
  ]
}
```

**Invalid example** (annotation name not found in space)
```json
{
  "project_id": "proj_abc123",
  "annotations": [
    {"record_id": "span_abc", "values": [{"name": "nonexistent_config"}]}
  ]
}
```

**Invalid example** (time window exceeds 31 days)
```json
{
  "project_id": "proj_abc123",
  "start_time": "2025-01-01T00:00:00Z",
  "end_time": "2025-03-01T00:00:00Z",
  "annotations": [
    {"record_id": "span_abc", "values": [{"name": "relevance", "label": "good"}]}
  ]
}
```

<Note>This endpoint is in beta, read more [here](https://arize.com/docs/ax/rest-reference#api-version-stages).</Note>

## Request body

- AnnotateSpansRequest — Batch annotation request for project spans.
  - `project_id` string, required — The project (model) ID whose spans are being annotated.
  - `start_time` string, date-time — Start of the time range for span lookup. Optional; defaults to 31 days ago.
  - `end_time` string, date-time — End of the time range for span lookup. Optional; defaults to now.
  - `granularity` 'SPAN' | 'TRACE' — Granularity of an annotation queue record. - SPAN: The record represents a span. - TRACE: The record represents a trace.
  - `annotations` AnnotateRecordInput[], required — Batch of span annotations to write. Up to 1000 spans per request.
    - `record_id` string, required — The record identifier (span ID, dataset example ID, or experiment run ID, depending on the endpoint).
    - `values` AnnotationInput[], required — One or more annotation values to set on this record.
      - `name` string, required — The annotation config name
      - `score` number, double — Numeric score for the annotation. Omit to leave unchanged.
      - `label` string — Categorical label for the annotation. Omit to leave unchanged.
      - `text` string — Free-form text note for the annotation. Omit to leave unchanged.

## Response `202`

Annotations accepted. Writes are idempotent; retry on failure is safe.

## Other responses

- `400` — Invalid request
- `401` — Authentication is required
- `403` — Insufficient permissions to access this resource
- `404` — Not found
- `422` — Unprocessable entity
- `429` — Rate limit exceeded

---

[API](https://skmtc.net/arize-ai/apis/arize-rest-api.md) · [All operations](https://skmtc.net/arize-ai/apis/arize-rest-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/arize-ai/arize-rest-api/versions/2ce448f1de13/schema)
