# Tag and organize your monitors

**Tags** are free-text labels on a monitor - `prod`, `client-acme`, `eu`, `payments`. They cost nothing, need no
setup, and make everything else easier once you have more than a handful of monitors: filtering the dashboard,
picking a set for a bulk edit, scoping an API query or a report.

## Settings reference

| Setting (app label) | API field (v2) | Type / allowed values | Default | Plan limits | What it does for you |
|---|---|---|---|---|---|
| **Tags** (Main Settings) | `tags` | Array of non-empty strings; each is trimmed | None | None | The monitor's full tag set. On update it **replaces** the set; `[]` or `null` clears it. |
| **Tags to add** (bulk edit) | `addTags` (update only) | Array of strings | - | None | Adds tags, keeping the monitor's other tags. |
| **Tags to remove** (bulk edit) | `removeTags` (update only) | Array of strings | - | None | Removes tags (exact, case-sensitive match); a tag that is not there is ignored. |
| **Default tags** (Profile -> Defaults) | - (app only) | Comma-separated text | None | None | Tags every monitor created in the app starts with. |

## The rules

- **A tag is a whole string.** Filtering by `prod` matches monitors tagged `prod`, not `production` or `prod-eu`.
- **Commas separate tags.** In the app, typing a comma (or pressing Enter) closes the current tag. Tags are
  stored as one comma-separated list, so a comma sent inside a tag through the API is read back as **two** tags.
  Don't put commas in tags.
- **No wildcards in tag filters.** Characters such as `*`, `%`, `_` and `[` are matched literally: `tag=50%`
  finds the tag `50%` and nothing else.
- **Case.** Removing a tag matches it exactly, including case, so `Prod` and `prod` are different tags to
  `removeTags`. Pick one spelling and stick to it.
- **Blank tags are refused** (`422 validation_failed`, `reason: "empty"`); surrounding spaces are trimmed.
- **No registry.** A tag exists while at least one monitor carries it. The list of tags in use comes from
  `GET /monitor?expand=summary` (`summary.tags`).

## Set it up in the app

**Tag one monitor**

1. On **Sites**, open the monitor (or **Add Monitor**) and expand **Main Settings**.
2. In **Tags**, type a tag and press Enter or type a comma. Repeat for more tags; click a tag's x to remove it.
3. **Save**.

**Filter by tag** - on **Sites**, open the tag filter (**Filter by tags**) and pick one or more tags. A monitor is
shown if it carries **any** of the selected tags; the tag filter combines with the type and status filters and the
search box.

**Add or remove a tag on many monitors** - select them on **Sites**, click **Edit**, fill **Tags to add** and/or
**Tags to remove** under **Monitor Settings**, then **Update**. Each monitor keeps its other tags. See
[Bulk operations](/monitors/bulk-operations/).

:::note[The search box is not a tag filter]
**Search by name or URL** looks only at names and addresses. It accepts `*`, but only as a separator: the
longest piece between the `*`s is searched as a plain substring, so `*.example.com` finds everything containing
`.example.com`.
:::

## Do it with the API or MCP

```bash
# Create with tags
curl -X POST https://api2.host-tracker.com/monitor \
  -H "Authorization: Bearer $HT_TOKEN" -H "Content-Type: application/json" \
  -d '{ "type": "http", "url": "https://shop.example.com/", "interval": 300,
        "locations": { "pools": ["allworld"] }, "tags": ["prod", "shop"] }'

# Add one tag and remove another, leaving the rest alone
curl -X PATCH "https://api2.host-tracker.com/monitor/$MONITOR_ID" \
  -H "Authorization: Bearer $HT_TOKEN" -H "Content-Type: application/json" \
  -d '{ "addTags": ["eu"], "removeTags": ["staging"] }'

# Every monitor tagged prod OR eu
curl "https://api2.host-tracker.com/monitor?tag=prod,eu" -H "Authorization: Bearer $HT_TOKEN"

# Tag every monitor tagged "shop" with "payments" (asynchronous job)
curl -X POST https://api2.host-tracker.com/monitor/bulk-update \
  -H "Authorization: Bearer $HT_TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: tag-shop-payments-1" \
  -d '{ "filter": { "tags": ["shop"] }, "patch": { "addTags": ["payments"] } }'
```

- `tag` on `GET /monitor` and `filter.tags` on the bulk operations both mean "any of these tags".
- `sort=tags` orders monitors by their tag list compared as text - in practice by the first tag alphabetically;
  untagged monitors come first.
- `expand=summary` adds `summary.byTag` (counts per tag) and `summary.tags` (all tags in use).
- MCP: **`create_monitor`** (`tags`, comma-separated), **`update_monitor`** (`tags` replaces, `addTags` /
  `removeTags` edit), **`list_monitors`** (`tag`), **`bulk_update_monitors`** (`filterJson`
  `{"tags":["shop"]}`, `patchJson` `{"addTags":["payments"]}`).

## Limits and gotchas

- `tags` together with `addTags` or `removeTags` in one body is refused (`422`, `reason: "conflicts_with_tags"`) -
  send the replacement or the edits, not both.
- `addTags` / `removeTags` on a create are refused (`reason: "update_only"`); use `tags`.
- A tag named in both `addTags` and `removeTags` ends up removed.
- Changing tags does not move a monitor's `updated` timestamp, so `GET /monitor?updatedSince=` will not report
  it; use the `monitor.updated` webhook to follow configuration edits.

## Related

- [The dashboard, explained](/getting-started/dashboard/)
- [Bulk operations](/monitors/bulk-operations/)
- [What a monitor is](/monitors/what-a-monitor-is/)
