# API de etiquetas

> Crear, listar, renombrar y eliminar el catálogo de etiquetas de toda la organización en la API para integradores.

Las etiquetas son un vocabulario de toda la organización, compartido por casos de prueba, defectos y ejecuciones. Hay un solo catálogo por organización, montado bajo `/api/v1/orgs/{orgUlid}/tags`. Cada etiqueta tiene un ULID asignado por el servidor, un nombre único dentro de la organización, un autor y tres contadores de uso.

Las entidades nunca referencian una etiqueta por ULID. Un caso de prueba, un defecto y una ejecución llevan todos `tags: string[]` — los nombres — y el servidor resuelve o crea la fila del catálogo detrás de ellos. Ver [Asociar etiquetas a entidades](#asociar-etiquetas-a-entidades).

## Crear

`POST /api/v1/orgs/{orgUlid}/tags`

Requiere el permiso de escritura de etiquetas (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden`.

### Cuerpo de la petición

| Campo  | Obligatorio | Notas                                                                                                            |
| ------ | ----------- | ---------------------------------------------------------------------------------------------------------------- |
| `name` | sí          | Recortado, 1–80 caracteres. Debe ser único dentro de la organización (ver [Reglas de nombre](#reglas-de-nombre)) |

El cuerpo es estricto: cualquier otra clave devuelve `422 validation_failed`.

### Respuesta

`201` con la etiqueta creada. Sus tres contadores valen `0`: una etiqueta recién creada no está asociada a nada. Ver [Campos de respuesta](#campos-de-respuesta).

Un nombre duplicado devuelve `409 conflict`.

### Ejemplo

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"smoke"}' \
  "https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags"
```

### Reglas de nombre

- El nombre se recorta antes de guardarse y antes de compararse, así que `"  smoke  "` y `"smoke"` son la misma etiqueta.
- La longitud se mide después del recorte: de 1 a 80 caracteres. Un nombre compuesto solo de espacios devuelve `422 validation_failed`.
- La unicidad es por organización y **distingue mayúsculas**. `Smoke` y `smoke` son dos etiquetas distintas y pueden coexistir; solo una repetición exacta tras el recorte devuelve `409 conflict`. El mismo nombre se acepta en otra organización.

## Listar

`GET /api/v1/orgs/{orgUlid}/tags`

Sin control de permisos: cualquier miembro de la organización puede leer el catálogo. Paginación por página.

El orden es **`name` ascendente** y es fijo: no hay parámetros de ordenación.

### Parámetros de consulta

| Parámetro    | Notas                                                                                                                                                                                                                                                                     |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page`       | Número de página (base 1, por defecto `1`)                                                                                                                                                                                                                                |
| `pageSize`   | Elementos por página (por defecto `50`, máximo `200`)                                                                                                                                                                                                                     |
| `q`          | Búsqueda de subcadena en el nombre, sin distinguir mayúsculas, máximo 80 caracteres                                                                                                                                                                                       |
| `authorUlid` | ULIDs de usuario separados por coma; coincide con las etiquetas creadas por cualquiera de ellos. Un ULID que no resuelve a ningún miembro no aporta nada, y un filtro que no resuelve a ningún miembro no coincide con nada en lugar de degradar a un listado sin filtrar |

La consulta es estricta: un parámetro mal escrito (`authorUlids`, `pageSizes`) devuelve `422 validation_failed` en vez de descartarse en silencio.

### Respuesta

`200` con `{ items, page, pageSize, total }`. Cada ítem sigue el [formato de respuesta](#campos-de-respuesta).

### Ejemplo

```bash
curl -sS \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags?q=smoke&pageSize=100"
```

## Actualizar

`PATCH /api/v1/orgs/{orgUlid}/tags/{tagUlid}`

Requiere el permiso de escritura de etiquetas (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden`.

### Cuerpo de la petición

| Campo  | Obligatorio | Notas                                                                       |
| ------ | ----------- | --------------------------------------------------------------------------- |
| `name` | sí          | El nuevo nombre. Mismas reglas de recorte, longitud y unicidad que al crear |

`name` es el único campo, así que un cuerpo vacío devuelve `422 validation_failed`.

### Respuesta

`200` con la etiqueta renombrada, con la misma forma que devuelve `POST` — y con los contadores **reales** de la etiqueta, no ceros. El cambio de nombre alcanza a todos los casos de prueba, defectos y ejecuciones que la llevan, sin desasociar ni modificar ninguno.

Un nombre que ya tiene otra etiqueta devuelve `409 conflict`. Un `{tagUlid}` fuera de la organización devuelve `404 not_found`; uno mal formado devuelve `422 validation_failed`.

### Ejemplo

```bash
curl -sS -X PATCH \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"smoke-suite"}' \
  "https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags/01JTAGXXXXXXXXXXXXXXXXXXXXX"
```

## Eliminar

`DELETE /api/v1/orgs/{orgUlid}/tags/{tagUlid}`

Requiere el permiso de eliminación de etiquetas (que tienen `owner`/`admin`); tanto un `member` como un `viewer` reciben `403 forbidden`. La separación es deliberada: eliminar una etiqueta la desasocia de todos los casos de prueba, defectos y ejecuciones que la llevan, y eso llega más lejos que ampliar el vocabulario.

Devuelve `204`. Las entidades quedan intactas: solo se eliminan la fila de la etiqueta y sus asociaciones. Un `{tagUlid}` fuera de la organización devuelve `404 not_found`.

### Ejemplo

```bash
curl -sS -X DELETE \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags/01JTAGXXXXXXXXXXXXXXXXXXXXX"
```

## Borrado en lote

`POST /api/v1/orgs/{orgUlid}/tags/bulk-delete`

Requiere el permiso de eliminación de etiquetas (que tienen `owner`/`admin`); tanto un `member` como un `viewer` reciben `403 forbidden`.

### Cuerpo de la petición

| Campo   | Obligatorio | Notas                                                                                      |
| ------- | ----------- | ------------------------------------------------------------------------------------------ |
| `ulids` | sí          | De 1 a 100 ULIDs de etiqueta. Un arreglo vacío o excedido devuelve `422 validation_failed` |

### Respuesta

`200` con `{ deleted: number }`: cuántas filas se eliminaron realmente. El lote es tolerante por diseño: un ULID que esta organización no posee, o uno que un borrado concurrente ya se llevó, se omite en lugar de hacer fallar la petición. Por eso `deleted` no siempre coincide con el tamaño del lote, y es la única señal de que tu selección contenía algo que no te pertenece.

Cada borrado desasocia la etiqueta exactamente igual que el `DELETE` individual.

### Ejemplo

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ulids":["01JTAGAXXXXXXXXXXXXXXXXXXXX","01JTAGBXXXXXXXXXXXXXXXXXXXX"]}' \
  "https://app.probara.net/api/v1/orgs/01JXXXXXXXXXXXXXXXXXXXXXXXXX/tags/bulk-delete"
```

## Campos de respuesta

`POST`, `PATCH` y cada ítem del listado devuelven un objeto de etiqueta con los siguientes campos. El objeto es estricto: no viaja ninguna otra clave.

| Campo         | Tipo           | Notas                                                                                |
| ------------- | -------------- | ------------------------------------------------------------------------------------ |
| `ulid`        | string (ULID)  | Identificador único asignado por el servidor                                         |
| `name`        | string         | El nombre de la etiqueta, tal cual lo llevan las entidades                           |
| `createdBy`   | object \| null | Resumen del autor, o `null` (ver [Autor](#autor))                                    |
| `caseCount`   | number         | Casos de prueba que llevan la etiqueta (ver [Contadores de uso](#contadores-de-uso)) |
| `runCount`    | number         | Ejecuciones que llevan la etiqueta                                                   |
| `defectCount` | number         | Defectos que llevan la etiqueta                                                      |
| `createdAt`   | number         | Milisegundos desde epoch Unix                                                        |
| `updatedAt`   | number         | Milisegundos desde epoch Unix                                                        |

`createdBy`, cuando existe, lleva `ulid`, `displayName`, `avatarKey`, `avatarVersion` y `positionTitle`.

### Contadores de uso

Los tres contadores no son totales de toda la organización. Hay dos propiedades que le importan a un integrador:

- **Están acotados a los proyectos que quien llama puede leer.** Una etiqueta asociada a un caso de un proyecto al que quien llama no tiene acceso no se cuenta para esa credencial. Dos credenciales pueden leer contadores distintos para la misma etiqueta con toda legitimidad, y ninguno está desactualizado.
- **Incluyen los casos de prueba archivados (en la papelera).** Es deliberado: una etiqueta cuyo único uso está archivado no debe marcar `0` e invitar a un borrado que le quitaría la etiqueta al caso en cuanto se restaure.

Un `0` significa entonces «nada visible para quien llama lleva la etiqueta», no «la etiqueta no se usa».

### Autor

`createdBy` es `null` cuando la etiqueta no tiene un autor resoluble. Hay dos casos distintos que lo producen:

- La etiqueta la creó un **token de API o un agente MCP**. Una etiqueta así no tiene autor humano por diseño.
- La cuenta del autor fue borrada.

Una etiqueta cuyo autor simplemente dejó la organización sigue llevando su resumen.

## Asociar etiquetas a entidades

Los casos de prueba, los defectos y las ejecuciones llevan `tags: string[]` — **nombres, no ULIDs** — tanto en la petición como en la respuesta. Las ejecuciones ganaron el campo junto con este catálogo: como máximo 50 nombres por ejecución, de 1 a 80 caracteres cada uno, en `POST`, en `PATCH` y en la respuesta de la ejecución.

El servidor resuelve cada nombre enviado contra el catálogo y **crea la fila cuando el nombre es nuevo**, así que enviar un nombre que el catálogo nunca vio acuña la etiqueta: no hace falta un `POST /tags` aparte. El arreglo del cuerpo de creación asocia; un arreglo en `PATCH` reemplaza el conjunto completo de etiquetas de la entidad.

De este contrato basado en nombres se desprenden dos consecuencias que conviene contemplar en el código:

- Un `[]` enviado (y un `null` enviado) vacía el conjunto, y la entidad se lee de vuelta con `tags: null`, no con `tags: []`. Omitir la clave deja el conjunto existente intacto.
- El arreglo devuelto siempre viene en **orden alfabético**, nunca en el orden de envío. No lo compares por posición con lo que enviaste.

Eliminar una etiqueta del catálogo la quita de todas las entidades que la llevaban; las entidades quedan intactas por lo demás y sus arreglos `tags` simplemente vuelven más cortos.

## Herramientas MCP

Las mismas cinco operaciones se exponen a los clientes MCP como `list_tags`, `create_tag`, `update_tag`, `delete_tag` y `bulk_delete_tags`. Cada una toma `org_id` (de `get_current_organization`) y aplica los mismos permisos que la ruta HTTP que tiene detrás. Una etiqueta creada por un agente MCP tiene `createdBy: null`, como describe [Autor](#autor). Ver [Herramientas MCP](https://docs.probara.net/es/developers/mcp-tools/).
