VitrinaAPI

Custom attributes

Each workspace's own fields: the definition once, then the value on each contact or conversation.

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/custom-attributesList attribute definitions
POST/custom-attributesDefine an attribute
DELETE/custom-attributes/{id}Delete a definition
GET/custom-attributes/{id}Fetch one definition
PUT/custom-attributes/{id}Update a definition
GET/custom-attributes/undefined-keysKeys with data but no definition
DELETE/custom-attributes/valuesDestroy the stored values of a key

GET /custom-attributes

List attribute definitions

The catalogue, optionally narrowed to one entity_type. Every row carries value_count and record_count — how much data is stored under that key and across how many records — so a delete dialog can state the blast radius without a second call.

ParameterInTypeRequiredConstraints
entity_typequerycontact \conversation \ticket \
curl https://api.vitrinadev.com/api/v1/custom-attributes \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "13131313-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "entity_type": "conversation",
      "key": "numero_de_caso",
      "label": "Número de caso",
      "data_type": "text",
      "options": [],
      "description": "El folio que el cliente cita al escribir",
      "required": false,
      "display_order": 1,
      "active": true,
      "multiline": false,
      "unique_values": false,
      "created_at": "2026-09-22T11:06:33.354Z",
      "updated_at": "2026-09-22T11:06:33.354Z",
      "value_count": 1,
      "record_count": 1
    }
  ]
}

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

POST /custom-attributes

Define an attribute

key is lowercase alphanumeric with underscores and is permanent — there is no rename, here or later.

Creating a key adopts data. If values were already stored under it, this definition takes them over immediately: they gain a label and a type without being rewritten. That is the intended way to formalise what the agent has been collecting, and it means a create is not always a blank field.

Because of that, a create asking for unique_values runs the same duplicate scan a later flip would, and 409s the same way — a workspace can never acquire a unique field that its existing data already violates. unique_values means no two records in the workspace may hold the same value, compared trimmed and case-insensitively; empty and null values never collide. It cannot be set on a file or boolean attribute — a unique document is meaningless and a unique boolean would cap the whole workspace at two records.

Turning it ON scans the existing data first and answers 409 listing the offending values and the records holding them, capped at 20 values with up to 10 records each. A 409 means nothing changed — the scan runs before anything is written and the rest of the change lands in one transaction with it, so a rename and a uniqueness flip in the same save either both apply or neither does. Turning it OFF is always allowed.

data_type shapes how values are validated when written. rut módulo-11 validates and stores the Chilean tax id canonically (19312475-5); file is contact-scoped ONLY, since documents are stored per contact and no other scope has an upload path. Type validation is write-time and never retroactive: changing a key to rut does not re-check or rewrite values already stored under it.

Answers 201.

Body

FieldTypeRequiredConstraints
entity_typecontact \conversation \ticket \
keystringyesmín. 1, máx. 100, patrón ^[a-z0-9_]+$
labelstringyesmín. 1, máx. 200
data_typetext \number \boolean \
optionsany[]—
descriptionstring | null—máx. 2000
requiredboolean—
display_orderinteger—
activeboolean—
multilineboolean—
unique_valuesboolean—
curl -X POST https://api.vitrinadev.com/api/v1/custom-attributes \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "conversation",
    "key": "numero_de_caso",
    "label": "Número de caso",
    "data_type": "text",
    "description": "El folio que el cliente cita al escribir",
    "display_order": 1
  }'

Example response (201)

{
  "data": {
    "id": "13131313-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "entity_type": "conversation",
    "key": "numero_de_caso",
    "label": "Número de caso",
    "data_type": "text",
    "options": [],
    "description": "El folio que el cliente cita al escribir",
    "required": false,
    "display_order": 1,
    "active": true,
    "multiline": false,
    "unique_values": false,
    "created_at": "2026-09-22T11:06:33.354Z",
    "updated_at": "2026-09-22T11:06:33.354Z"
  }
}

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

DELETE /custom-attributes/{id}

Delete a definition

Non-destructive by default. Removes the definition and leaves every stored value in place as an orphan — invisible in the UI, still in the database, and re-adopted automatically if the key is ever created again. Deleting a field is therefore reversible; that is the point.

?purge_values=true opts in to destroying the data as well, including reaping uploaded files. It matters most for a file attribute, where the "harmless orphan" is a customer’s document still sitting in the bucket and still downloadable.

The flag fails closed: anything that is not exactly true — absent, false, 1, a typo — leaves the data alone. Values are purged BEFORE the definition is removed, so a failed purge leaves both intact and the call retryable, rather than a stranded key whose size nobody can see any more.

A purge on a ticket or company definition is a truthful no-op — those scopes have no value table.

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

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

GET /custom-attributes/{id}

Fetch one definition

The definition on its own, without the value_count / record_count aggregates the list carries.

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

Example response (200)

{
  "data": {
    "id": "13131313-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "entity_type": "conversation",
    "key": "numero_de_caso",
    "label": "Número de caso",
    "data_type": "text",
    "options": [],
    "description": "El folio que el cliente cita al escribir",
    "required": false,
    "display_order": 1,
    "active": true,
    "multiline": false,
    "unique_values": false,
    "created_at": "2026-09-22T11:06:33.354Z",
    "updated_at": "2026-09-22T11:06:33.354Z"
  }
}

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

PUT /custom-attributes/{id}

Update a definition

A PUT that behaves as a PATCH; at least one field is required. key and entity_type are not accepted — both are create-only, because changing either would orphan every value stored under the old pair. They are stripped from the body rather than rejected, so sending them silently does nothing.

unique_values means no two records in the workspace may hold the same value, compared trimmed and case-insensitively; empty and null values never collide. It cannot be set on a file or boolean attribute — a unique document is meaningless and a unique boolean would cap the whole workspace at two records.

Turning it ON scans the existing data first and answers 409 listing the offending values and the records holding them, capped at 20 values with up to 10 records each. A 409 means nothing changed — the scan runs before anything is written and the rest of the change lands in one transaction with it, so a rename and a uniqueness flip in the same save either both apply or neither does. Turning it OFF is always allowed.

Changing data_type to file is refused unless the definition is contact-scoped. Changing a type does NOT re-validate or convert stored values — they keep whatever they hold until something writes them again, so a switch to rut or number can leave rows that would not be accepted today.

options is replaced wholesale. Removing an option a record already holds does not clear that record’s value.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
labelstring—mín. 1, máx. 200
data_typetext \number \boolean \
optionsany[]—
descriptionstring | null—máx. 2000
requiredboolean—
display_orderinteger—
activeboolean—
multilineboolean—
unique_valuesboolean—
curl -X PUT https://api.vitrinadev.com/api/v1/custom-attributes/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "required": true
  }'

Example response (200)

{
  "data": {
    "id": "13131313-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "entity_type": "conversation",
    "key": "numero_de_caso",
    "label": "Número de caso",
    "data_type": "text",
    "options": [],
    "description": "El folio que el cliente cita al escribir",
    "required": true,
    "display_order": 1,
    "active": true,
    "multiline": false,
    "unique_values": false,
    "created_at": "2026-09-22T11:06:33.354Z",
    "updated_at": "2026-09-22T11:06:33.354Z"
  }
}

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

GET /custom-attributes/undefined-keys

Keys with data but no definition

The "Claves sin definición" panel: attribute keys that have stored VALUES in this scope but no definition describing them — usually because the agent captured something nobody had declared yet.

Each row reports uses, distinct_records and a breakdown by source (tool, admin, system, agent), so a busy key written by the agent is distinguishable from a one-off typo. Ordered most-used first and capped at 200 keys.

From here a key has two ends: POST /custom-attributes with the same key adopts it and every value under it, or DELETE /custom-attributes/values clears it away.

entity_type is required and limited to contact or conversation — the other two scopes store no per-record values, so the question does not apply. Registered before /\{id\}.

ParameterInTypeRequiredConstraints
entity_typequerycontact \conversationyes
curl https://api.vitrinadev.com/api/v1/custom-attributes/undefined-keys \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "key": "Talla camisa",
      "entity_type": "conversation",
      "count": 3
    }
  ]
}

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

DELETE /custom-attributes/values

Destroy the stored values of a key

Destructive and not undoable. Deletes every value under one (entity_type, key) across the whole workspace and reaps the uploaded file behind each file value. Answers \{ removed, files_reaped \}.

This targets DATA, never the catalogue. Called on a stranded key it finishes the cleanup undefined-keys started; called on a key that still has a definition it empties the field and leaves the field in place, which is a legitimate "start this field over".

key is taken verbatim and is deliberately not normalised — keys written before write-time normalisation existed can look like "Talla camisa", and normalising the parameter would make exactly those stranded rows unpurgeable. Send the key exactly as undefined-keys reports it.

File reaping is best-effort: the rows are gone before storage is touched, so a bucket failure lowers files_reaped rather than failing the purge. Registered before /\{id\} so values is never read as an id.

ParameterInTypeRequiredConstraints
entity_typequerycontact \conversationyes
keyquerystringyesmín. 1, máx. 200
curl -X DELETE https://api.vitrinadev.com/api/v1/custom-attributes/values \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "removed": 3,
    "files_reaped": 0
  }
}

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

On this page