# Contact tags

Tags are the fastest way to segment the CRM: "cliente", "lead-caliente", "Participante". In Met they live in two places, and it pays to keep them apart:

- **The catalog** (`met.contactTags`): the tags that exist in the workspace, even if no contact has them yet. It is what you see in Settings and in the pickers of the panel, broadcasts and flows.
- **The assignment** (`met.contacts.addTag` and friends): which tags each contact has. It is stored in the contact's `$tags` system field.

Reading requires `contacts:read`; writing, `contacts:write`. Both work with a test key. Routes are flat (`/contact-tags`, `/contacts/…`): they operate on the key's workspace.

## The catalog

```ts
const tags = await met.contactTags.list();
// [{ id: 74, name: 'lead-caliente' }, { id: 75, name: 'Participante' }]

const vip = await met.contactTags.create({ name: 'VIP' });
await met.contactTags.rename(vip.id, 'Cliente VIP');
await met.contactTags.delete(vip.id); // { ok: true }
```
```python
tags = met.contact_tags.list()

vip = met.contact_tags.create({"name": "VIP"})
met.contact_tags.rename(vip["id"], "Cliente VIP")
met.contact_tags.delete(vip["id"])
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/contact-tags \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"VIP"}'
```

- `list()` returns **the whole** catalog, sorted by name, including tags nobody uses.
- The name is unique in the workspace **regardless of case**: `VIP` and `vip` are the same tag, and creating the second one responds `409`. Up to 60 characters; stored without surrounding spaces.
- `create()` only registers it in the catalog: it does not assign it to any contact.
- `rename()` rewrites the name **on every contact that had it**, so no orphan tags are left behind.
- `delete()` removes it from the catalog **and from every contact**. It cannot be undone.

## Adding and removing a contact's tags

```ts
await met.contacts.addTag(4821, 'lead-caliente');
await met.contacts.removeTag(4821, 'frío');

const theirs = await met.contacts.tags(4821); // ['lead-caliente', 'Participante']
```
```python
met.contacts.add_tag(4821, "lead-caliente")
met.contacts.remove_tag(4821, "frío")

theirs = met.contacts.tags(4821)
```

Adding a tag to a contact that is not in the catalog registers it in the catalog on its own. Adding one the contact already has does nothing.

## Many contacts at once

One tag on a list of contacts, in a single call (up to 500 per call):

```ts
const r = await met.contacts.bulkAddTags({ contact_ids: [4821, 4822, 4830], tag: 'Feria 2026' });
// { updated: 2, requested: 3, tag: 'Feria 2026' }

await met.contacts.bulkRemoveTags({ contact_ids: [4821, 4822], tag: 'Feria 2026' });
```
```python
r = met.contacts.bulk_add_tags({"contact_ids": [4821, 4822, 4830], "tag": "Feria 2026"})
met.contacts.bulk_remove_tags({"contact_ids": [4821, 4822], "tag": "Feria 2026"})
```

`updated` only counts the contacts that actually changed: those that already had the tag, or ids that are not contacts of this workspace, do not add up. It is **one** tag per call (`tag`, not a list).

## Filtering by tag

`contacts.list()` accepts conditions on the contact's fields in `filters`, a JSON array. For tags, the field is `$tags` and the operators are `has` and `not_has`:

```ts
const { data, total } = await met.contacts.list({
  filters: JSON.stringify([{ field: '$tags', op: 'has', value: 'lead-caliente' }]),
});
```
```python
import json

page = met.contacts.list(filters=json.dumps([{"field": "$tags", "op": "has", "value": "lead-caliente"}]))
```

A malformed condition is **ignored** instead of raising an error: check `total` before trusting the filter. To know which tags exist, adding up the catalog's and those contacts have even if they are not registered, use `met.contacts.tagsDistinct()`.

## Hearing about changes

There is no tag event of its own. Adding or removing a tag on a contact changes the contact, and that goes out as `contact.updated` (scope `contacts:read`; see [Webhooks](webhooks.html)).

## Common errors

| Code | What happened |
|---|---|
| `400` `validation_error` | The name is empty or longer than 60 characters; in bulk, more than 500 contacts or an empty list. |
| `404` `not_found` | The tag (in `rename` or `delete`) or the contact does not exist in this workspace. |
| `409` `conflict` | A tag with that name already exists, ignoring case. |
| `403` `missing_scope` | The key does not carry `contacts:write` (or `contacts:read` for reading). |
