# Labels and system labels (/extras/labels-and-system-labels)

<!-- agent-signals: reading_time_min: 3 · est_tokens: 1218 · updated: 2026-09-06 -->
Related: [Deliverability fundamentals](/extras/deliverability-fundamentals.md), [Security and compliance](/extras/security-compliance.md), [Account lifecycle](/extras/account-lifecycle.md), [AI Employee With Its Own Inbox](/examples/ai-employee-with-its-own-inbox.md)



# Apply caller labels and read system labels

Caller-managed labels track application state on messages and threads, while AgentMail-owned system labels expose delivery state and control visibility. Use this to change labels safely, avoid the label strings the service reserves, and diagnose a resource hidden by a restricted label before treating it as missing data.

## Do this

Add and remove caller-managed labels on one message, then audit the change in the inbox event trail.

```bash
# 1. Change labels in one targeted request
curl -X PATCH "https://api.agentmail.to/v0/inboxes/<inbox_id>/messages/<message_id>" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "add_labels": ["needs-review"], "remove_labels": ["triaged"] }'

# 2. Audit label changes (reverse chronological by default)
curl "https://api.agentmail.to/v0/inboxes/<inbox_id>/events" \
  -H "Authorization: Bearer $AGENTMAIL_API_KEY"
```

Keep caller labels in an application namespace such as `needs-review` or `customer-123`. Never submit a service-owned label string.

## SDK

Install: `pip install agentmail` (Python).

* Update message labels: Python `client.inboxes.messages.update(inbox_id="support@example.com", message_id="<message-id@example.com>", add_labels=["needs-review"], remove_labels=["triaged"])`

Full reference: `/integrations/sdks-and-cli`.

## Facts

* Caller-managed labels are lowercase string tags on messages and threads. A resource holds at most 256 labels.
* The `message_update` permission is required to change message labels and is also required to change thread labels.
* `PATCH /v0/inboxes/{inbox_id}/messages/{message_id}` takes `add_labels` and `remove_labels` in the JSON body. The response contains the message's `message_id` and its resulting `labels`.
* System labels are service-owned state. A system label can coexist with caller-managed labels on the same resource, for example `received` plus `needs-review`, and a caller label update leaves system labels unchanged.
* Protected system labels, complete list: `received`, `sent`, `bounced`, `complained`, `delayed`, `delivered`, `rejected`, `opened`, `scheduled`. A request that includes any of them as caller label input is rejected with a `400` naming the label.
* Restricted visibility labels, complete list: `spam`, `blocked`, `unauthenticated`, `trash`. List requests exclude resources carrying these labels by default.
* Each restricted label has a matching API-key read permission, a boolean permission field set when the key is created or updated: `spam` needs `label_spam_read`, `blocked` needs `label_blocked_read`, `unauthenticated` needs `label_unauthenticated_read`, `trash` needs `label_trash_read`.
* A restricted-label resource appears in a list result only when the credential is scoped to the resource, the credential carries the matching label read permission, and the list request passes the matching include option such as `include_spam=true`. All conditions are required.
* A direct lookup without the required label-read permission reports the resource as not found. That response intentionally does not distinguish a hidden resource from an incorrect identifier.
* Label audit trail: `GET /v0/inboxes/{inbox_id}/events` lists inbox events, reverse chronological by default. Each event identifies the `event_type` (`label.added` or `label.removed`), `message_id`, the changed `label`, `event_at`, and `event_id`. Paginate when reconstructing a longer history.

## Not supported

* Callers cannot add or remove system labels through label creation or update input.
* `received`, `sent`, `bounced`, `complained`, `delayed`, `delivered`, `rejected`, `opened`, and `scheduled` are rejected in `labels`, `add_labels`, and `remove_labels`.
* `include_spam=true` alone does not reveal spam-labeled resources when the credential lacks `label_spam_read`. The include option and the permission are both required, and the same pairing applies to `blocked`, `unauthenticated`, and `trash`.
* A not-found response on a direct lookup does not prove the resource was deleted. Check the credential's scope, the relevant label permission, and the list request's include option before assuming data is gone.

## Verify

Send the `PATCH` request above, then read the response's `labels` array. It contains `needs-review` and no longer contains `triaged`. `GET /v0/inboxes/{inbox_id}/events` shows a matching `label.added` event for the message.

## Related

* [/core/conversations](/core/conversations) - work threads and messages and apply label updates in that flow.
* [/extras/deliverability-fundamentals](/extras/deliverability-fundamentals) - the delivery conditions behind labels such as `bounced` and `complained`.
* [/core/inbound-control](/core/inbound-control) - the inbound filtering that assigns `spam`, `blocked`, and `unauthenticated`.
