Annotations
Read, save, and delete annotations on individual images. Save is a full overwrite — pass the complete list every time.
Annotations follow the canonical Pictograph JSON schema. See
Annotation format for the full spec. The class
label field is name (not class). Polygons use multi-ring paths, not
flat coordinate arrays.
Every example below shows the Python SDK call and the equivalent raw REST
request. The REST examples authenticate with an X-API-Key header; set
PICTOGRAPH_API_KEY in your shell to copy-and-run them.
from pictograph import Client
client = Client() # reads PICTOGRAPH_API_KEY
get
Fetch the typed annotation list attached to an image.
annotations = client.annotations.get("img-uuid-1")
for ann in annotations:
print(ann.name, ann.type)
curl -s "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns list[Annotation]: a discriminated union over
BBoxAnnotation / PolygonAnnotation / PolylineAnnotation /
KeypointAnnotation. An image with no annotations returns []
(never raises for the “no annotations” case, only for “no such image”).
save
Replace the image’s annotations with the supplied list. Full overwrite: existing annotations are dropped.
| Arg | Type | Notes |
|---|---|---|
image_id | str | Image UUID |
annotations | Sequence[Annotation] | Pydantic-validated client-side; backend re-validates |
from pictograph import BBoxAnnotation, BoundingBox, PolygonAnnotation, PolygonGeometry, Point
result = client.annotations.save("img-uuid-1", [
BBoxAnnotation(
id="ann-1",
name="person",
bounding_box=BoundingBox(x=100, y=200, w=50, h=80),
),
PolygonAnnotation(
id="ann-2",
name="car",
polygon=PolygonGeometry(paths=[
[Point(x=0, y=0), Point(x=10, y=0), Point(x=10, y=10)],
]),
),
])
print(result.previous_count, "→", result.new_count, result.status)
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"image_id": "img-uuid-1",
"annotations": [
{"id": "ann-1", "name": "person", "type": "bbox",
"bounding_box": {"x": 100, "y": 200, "w": 50, "h": 80}},
{"id": "ann-2", "name": "car", "type": "polygon",
"polygon": {"paths": [[{"x": 0, "y": 0}, {"x": 10, "y": 0}, {"x": 10, "y": 10}]]}}
]
}'
Returns SaveResult with image_id, previous_count, new_count, status.
The image_id in the URL must match the one in the body. Polygons may omit
bounding_box on save: the backend computes the enclosing rectangle
server-side. Requires member+ role.
bulk_save
Save annotations for many images in one call. Same full-overwrite semantics as
save — each image’s existing annotations are dropped. Up to 200 images per
call. A bad image id lands in failed rather than failing the whole batch.
from pictograph import BBoxAnnotation, BoundingBox
result = client.annotations.bulk_save({
"img-uuid-1": [
BBoxAnnotation(id="ann-1", name="person",
bounding_box=BoundingBox(x=100, y=200, w=50, h=80)),
],
"img-uuid-2": [
BBoxAnnotation(id="ann-2", name="car",
bounding_box=BoundingBox(x=10, y=20, w=30, h=40)),
],
})
print(result.saved_count, "saved,", len(result.failed), "failed")
for fail in result.failed:
print(fail.image_id, fail.error)
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/bulk" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"saves": [
{"image_id": "img-uuid-1", "annotations": [
{"id": "ann-1", "name": "person", "type": "bbox",
"bounding_box": {"x": 100, "y": 200, "w": 50, "h": 80}}
]},
{"image_id": "img-uuid-2", "annotations": [
{"id": "ann-2", "name": "car", "type": "bbox",
"bounding_box": {"x": 10, "y": 20, "w": 30, "h": 40}}
]}
]
}'
Returns BulkSaveResult with saved (list[SaveResult]), failed
(list[BulkSaveFailure]), and saved_count. Requires member+ role.
delete
Remove every annotation from the image. Equivalent to save(image_id, [])
but uses DELETE and requires admin+ role.
result = client.annotations.delete("img-uuid-1")
print(result.deleted_count)
curl -s -X DELETE "https://api.pictograph.io/api/v1/developer/annotations/img-uuid-1" \
-H "X-API-Key: $PICTOGRAPH_API_KEY"
Returns DeleteResult with image_id and deleted_count.
rename_class
Rename an annotation class across a whole dataset in one call — both the
class ontology entry (the class list the editor shows) and every stored
annotation carrying the old name. One set-based server-side statement, not a
per-image loop — use it for label-taxonomy cleanup (car → vehicle).
Requires member+ role.
result = client.annotations.rename_class(dataset.id, "car", "vehicle")
print(result.annotations_updated, "annotations across", result.images_updated, "images")
# result.config_updated is True when the ontology entry was renamed too
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/rename-class" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
-d '{"dataset_id": "<dataset-uuid>", "old_name": "car", "new_name": "vehicle"}'
pictograph annotations rename-class my-dataset car vehicle
The response is {"data": {dataset_id, old_name, new_name, images_updated, annotations_updated, config_updated}}. Renaming onto a class name that
already exists for the same annotation type returns 409 ConflictError
(merge the classes deliberately instead); a class that only exists on
annotations (not in the ontology) still renames, with
config_updated: false.
merge_class
Merge one class into another across a whole dataset: every annotation
labeled source_name is reassigned to target_name and the source class is
dropped from the ontology (the target is kept). This is the deliberate
counterpart to the rename_class 409 — use it to combine two classes
(car + auto → vehicle). One set-based server-side statement. Requires
member+ role.
result = client.annotations.merge_class(dataset.id, "auto", "vehicle")
print(result.annotations_updated, "annotations reassigned to", result.target_name)
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/merge-class" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
-d '{"dataset_id": "<dataset-uuid>", "source_name": "auto", "target_name": "vehicle"}'
pictograph annotations merge-class my-dataset auto vehicle
The response is {"data": {dataset_id, source_name, target_name, images_updated, annotations_updated, config_updated}}.
delete_class
Delete a class from a dataset’s ontology, optionally also removing every
annotation of that class in one set-based statement. With
delete_annotations=False (the default) only the ontology entry is removed and
existing annotations are left in place; True strips them too. Requires
member+ role.
# ontology only (annotations left as-is)
client.annotations.delete_class(dataset.id, "obsolete")
# also strip every annotation of the class
result = client.annotations.delete_class(dataset.id, "obsolete", delete_annotations=True)
print(result.annotations_removed, "annotations removed")
curl -s -X POST "https://api.pictograph.io/api/v1/developer/annotations/delete-class" \
-H "X-API-Key: $PICTOGRAPH_API_KEY" -H "Content-Type: application/json" \
-d '{"dataset_id": "<dataset-uuid>", "name": "obsolete", "delete_annotations": true}'
pictograph annotations delete-class my-dataset obsolete --with-annotations
The response is {"data": {dataset_id, name, config_updated, images_updated, annotations_removed}}.
Validation
The SDK Pydantic models reject malformed payloads at construction:
from pictograph import PolygonAnnotation, PolygonGeometry, Point
PolygonGeometry(paths=[[Point(x=0, y=0)]])
# ValidationError: paths[0] has 1 point(s); polygon ring requires >= 3
The backend re-validates on save as defense-in-depth: callers that
construct dicts directly will hit 422 ValidationError for the same
class of mistakes.
Common errors
| Status | Exception | Cause |
|---|---|---|
| 404 | NotFoundError | image_id doesn’t exist |
| 422 | ValidationError | class instead of name, flat polygon array, unknown class label |
| 400 | ApiError | URL image_id does not match the body image_id on save |
| 403 | ForbiddenError | save/rename_class require member+; delete requires admin+ role |
| 409 | ConflictError | rename_class onto a name that already exists for the same annotation type |
Auto-annotate workflow
If you want SAM3 to generate annotations rather than write them by hand,
see the auto-annotate resource.
The auto_annotate_dataset workflow saves annotations automatically;
the single-prompt methods return a PromptResult and you call save
yourself.