VitrinaAPI

Tags

The loose vocabulary hung off a conversation or a contact: attaching by name creates it on first use.

Beta
This may change at any time, with a changelog entry and a notice to recent callers — see versioning.

Download the full API pública projection: openapi.json.

Reply and resolve tickets explains this resource in prose, with runnable examples.

MethodPathWhat it does
GET/tagsList tags
POST/tagsCreate a tag, or get the existing one
DELETE/tags/{id}Delete a tag
GET/tags/{id}Fetch one tag
PUT/tags/{id}Rename a tag’s display name
GET/tags/suggestAsk the model to suggest tags for a conversation

GET /tags

List tags

q filters by name. limit defaults to 50 and caps at 200 — there is no cursor, so a workspace with more tags than the limit simply cannot page past it; narrow with q instead.

with_contact_counts=true adds how many contacts carry each tag. It is an aggregate over the join, so leave it off for a picker and turn it on for a management screen.

ParameterInTypeRequiredConstraints
qquerystringnomín. 1, máx. 120
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
with_contact_countsqueryanyno
curl https://api.vitrinadev.com/api/v1/tags \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "12121212-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "garantia",
      "display_name": "Garantía",
      "usage_count": 1,
      "created_at": "2026-09-22T11:06:38.372Z"
    },
    {
      "id": "12121212-0000-4000-8000-000000000002",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "name": "urgente",
      "display_name": "Urgente",
      "usage_count": 4,
      "created_at": "2026-09-22T11:06:38.372Z"
    }
  ]
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /tags

Create a tag, or get the existing one

Idempotent by slug, and the status code is how you tell which happened: 201 when the tag was created, 200 when one with that slug already existed and is being returned instead. Neither is an error, so a UI can submit a freshly typed name without checking first.

name is slugified to form the identity; display_name is what people see and defaults to name as typed. Submitting an existing slug with a different display_name returns the STORED row unchanged — this endpoint will not rename an existing tag, PUT /tags/\{id\} does that.

A name with no alphanumeric characters slugifies to nothing and is a 400. Concurrent creates of the same new name are safe: the loser of the race gets the winner’s row rather than an error.

Body

FieldTypeRequiredConstraints
namestringyesmín. 1, máx. 120
display_namestring—mín. 1, máx. 120
curl -X POST https://api.vitrinadev.com/api/v1/tags \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Garantía",
    "display_name": "Garantía"
  }'

Example response (200)

{
  "data": {
    "id": "12121212-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "garantia",
    "display_name": "Garantía",
    "usage_count": 1,
    "created_at": "2026-09-22T11:06:38.372Z"
  }
}

Answers: 200 · 201 · 400 · 401 · 403 · 404 · 409 · 429

DELETE /tags/{id}

Delete a tag

Removes the tag and every attachment to it, across conversations and contacts alike. A 404 for an id that is not there.

It does not stay deleted if anything still uses the name: attaching by name re-creates it, and so does an agent acting on a suggestion. A tag that keeps coming back is being written by something, not resurrected.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl -X DELETE https://api.vitrinadev.com/api/v1/tags/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Answers: 204 · 400 · 401 · 403 · 404 · 409 · 429

GET /tags/{id}

Fetch one tag

The tag by id, without the contact count the list can add.

ParameterInTypeRequiredConstraints
idpathuuidyes
curl https://api.vitrinadev.com/api/v1/tags/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "12121212-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "garantia",
    "display_name": "Garantía",
    "usage_count": 1,
    "created_at": "2026-09-22T11:06:38.372Z"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

PUT /tags/{id}

Rename a tag’s display name

display_name is the ONLY editable field, and it is required. The slug name is permanent: it is the identity every attachment and every create-by-name call resolves through, so renaming it would detach the tag from itself.

In practice that means a tag can be relabelled for people without anything breaking, but its canonical name stays whatever it was first typed as. To change the slug, create a new tag and re-attach.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
display_namestringyesmín. 1, máx. 120
curl -X PUT https://api.vitrinadev.com/api/v1/tags/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "display_name": "Garantía extendida"
  }'

Example response (200)

{
  "data": {
    "id": "12121212-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "name": "garantia",
    "display_name": "Garantía extendida",
    "usage_count": 1,
    "created_at": "2026-09-22T11:06:38.372Z"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

GET /tags/suggest

Ask the model to suggest tags for a conversation

Reads the conversation’s recent transcript and the workspace’s existing tags, and returns suggestions as \{ name, display_name, existing, tag_id \}.

existing: true means the slug is already a tag here and tag_id is set, so it can be attached directly. existing: false rows have a null tag_id and are attached by NAME, which creates them on first use — the suggestion itself creates nothing.

Suggestions are deduplicated by slug, so the model proposing both Prioridad and prioridad yields one row. Nothing is attached and nothing is stored; this is advisory.

Results are cached server-side per conversation, keyed off the newest message — a fresh message rotates the key automatically, so repeat opens of an unchanged conversation are served from cache with no LLM call. Pass force=true to bypass a live cache entry and recompute, overwriting it — this backs an explicit "refresh suggestions" action; do not set it on every load.

On a cache miss this is one LLM call over the conversation transcript — not free, and not something to poll. Registered before /\{id\} so suggest is never read as a tag id.

ParameterInTypeRequiredConstraints
conversation_idqueryuuidyes
forcequeryanyno
curl https://api.vitrinadev.com/api/v1/tags/suggest \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "name": "garantia",
      "display_name": "Garantía",
      "existing": true,
      "tag_id": "12121212-0000-4000-8000-000000000001"
    },
    {
      "name": "coordinacion-de-visita",
      "display_name": "coordinacion-de-visita",
      "existing": false,
      "tag_id": null
    }
  ]
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

On this page