Tickets
El caso que sobrevive al hilo: puede dueñar varias conversaciones, vive en una columna de un tablero y tiene responsable y SLA.
Descarga la proyección completa de la API pública: openapi.json.
Responder hilos y resolver tickets explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /tickets/{id} | Get ticket |
PUT | /tickets/{id}/assign | Assign a ticket to a teammate or a team |
POST | /tickets/{id}/claim | Claim a ticket for the caller |
PUT | /tickets/{id}/close | Resolve a ticket (the manual "done" action) |
GET | /tickets/{id}/conversations | The conversations this ticket owns |
GET | /tickets/{id}/messages | One timeline across every conversation on this ticket |
PUT | /tickets/{id}/snooze | Snooze a ticket until a wake-up time |
PUT | /tickets/{id}/stage | Move a ticket to another column |
PUT | /tickets/{id}/status | Set the ticket status |
GET | /tickets/stage/{stageId} | Tickets on one column of a board |
GET | /tickets/status-counts | Ticket counts by lifecycle state |
GET | /tickets/today-counts | Ticket counts for tickets opened today |
GET /tickets/{id}
Get ticket
The ticket row. origin_conversation_id is the thread it was opened from; every thread attached to it is GET /tickets/\{id\}/conversations. display_id (T-1247) is the label people quote — a path accepts it, nothing stores it.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
curl https://api.vitrinadev.com/api/v1/tickets/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "open",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000011",
"snoozed_until": null,
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": null,
"resolved_at": null,
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /tickets/{id}/assign
Assign a ticket to a teammate or a team
Send at least one of handler, assignee_user_id, team_id; an empty body is a 400. null on either id CLEARS it, omitting the key leaves it alone — two different requests, and the server tells them apart. Assigning drops a system line in the origin conversation naming the author («Camila», «Camila vía Claude», «CRM propio (API)»).
This is the key-friendly counterpart of POST /tickets/\{id\}/claim.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
handler | bot \ | human \ | external |
assignee_user_id | string | null | — | |
team_id | string | null | — |
curl -X PUT https://api.vitrinadev.com/api/v1/tickets/<id>/assign \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001"
}'Ejemplo de respuesta (200)
{
"data": [
{
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "open",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000011",
"snoozed_until": null,
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": null,
"resolved_at": null,
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /tickets/{id}/claim
Claim a ticket for the caller
Requires a workspace member — a Supabase session or a personal token acting as one — since the caller BECOMES the assignee, and an sk_* API key has no member behind it to assign the ticket to; it gets 403. An API key assigns to a SPECIFIC member instead: PUT /tickets/\{id\}/assign.
Claiming a ticket somebody else already holds is not an error: the response carries alreadyClaimed: true and the current assignee, so two people pressing at once get the same answer rather than a race. Takes tickets:claim, not tickets:write.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
curl -X POST https://api.vitrinadev.com/api/v1/tickets/<id>/claim \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"ticketId": "dddddddd-0000-4000-8000-000000000001",
"assigneeUserId": "11111111-0000-4000-8000-000000000001",
"alreadyClaimed": false
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /tickets/{id}/close
Resolve a ticket (the manual "done" action)
⚠ Despite the path, this RESOLVES: status becomes resolved, the handler flips to human, resolved_by/resolved_at are stamped and any snooze is cleared. It is the manual counterpart of the auto-resolve worker, and the path name predates the resolved/closed split. The terminal closed state is reached by the auto-close worker a grace window later, or by PUT /tickets/\{id\}/status with closed.
This is the operation that fires ticket.resolved.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
curl -X PUT https://api.vitrinadev.com/api/v1/tickets/<id>/close \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "resolved",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000011",
"snoozed_until": null,
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": "human",
"resolved_at": "2026-09-22T11:15:06.653Z",
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /tickets/{id}/conversations
The conversations this ticket owns
Every thread attached to the case, across channels — the WhatsApp conversation it started on and the email one the customer opened later. Each row carries the contact identity the inbox shows beside it, so a client does not need a contacts:read call to label the list.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
curl https://api.vitrinadev.com/api/v1/tickets/<id>/conversations \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "bbbbbbbb-0000-4000-8000-000000000001",
"external_id": "web:visitor-9f2c1a",
"channel": "web",
"brand": null,
"contact_id": "22222222-0000-4000-8000-000000000001",
"contact_name": "Rodrigo Pizarro",
"contact_phone": "+56977000021",
"messaging_account_id": "16161616-0000-4000-8000-000000000001",
"ticket_id": "dddddddd-0000-4000-8000-000000000001",
"last_message_date": "2026-09-22T11:08:10.882Z",
"created_at": "2026-09-22T11:06:33.744Z",
"updated_at": "2026-09-22T11:06:33.744Z"
},
{
"id": "bbbbbbbb-0000-4000-8000-000000000002",
"external_id": "[email protected]",
"channel": "email",
"brand": null,
"contact_id": "22222222-0000-4000-8000-000000000001",
"contact_name": "Rodrigo Pizarro",
"contact_phone": "+56977000021",
"messaging_account_id": "16161616-0000-4000-8000-000000000001",
"ticket_id": "dddddddd-0000-4000-8000-000000000001",
"last_message_date": "2026-09-22T12:02:11.000Z",
"created_at": "2026-09-22T12:01:44.000Z",
"updated_at": "2026-09-22T12:02:11.000Z"
}
],
"meta": {
"total": 2,
"ticket_id": "dddddddd-0000-4000-8000-000000000001"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /tickets/{id}/messages
One timeline across every conversation on this ticket
The case's whole history, interleaved in time: messages from every attached conversation, each stamped with the channel it arrived on, plus the system lines that record assignments and status changes (type: "event"). ?before pages backwards; the default page is 500 messages, the maximum 2 000.
This is a flattened view for reading. For the per-message delivery fields and the author of each one, read a conversation directly: GET /conversations/\{id\}/messages.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
limit | query | integer | no | ≥ 1, ≤ 2000, por defecto 500 |
before | query | string | no | date-time |
curl https://api.vitrinadev.com/api/v1/tickets/<id>/messages \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "eeeeeeee-0000-4000-8000-000000000001",
"conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"channel": "web",
"role": "user",
"content": "¿Me pueden confirmar la hora del jueves?",
"type": "text",
"tool_calls": null,
"tool_call_id": null,
"created_at": "2026-09-22T11:07:02.824Z"
},
{
"id": "eeeeeeee-0000-4000-8000-000000000002",
"conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"channel": "web",
"role": "system",
"content": "CRM propio (API) reasignó la conversación",
"type": "event",
"tool_calls": null,
"tool_call_id": null,
"created_at": "2026-09-22T11:06:54.411Z"
}
],
"meta": {
"ticket_id": "dddddddd-0000-4000-8000-000000000001",
"limit": 500
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /tickets/{id}/snooze
Snooze a ticket until a wake-up time
until is an ISO 8601 timestamp. The ticket leaves the open queue and comes back by itself; a new inbound message on any of its conversations wakes it early.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
until | string | sí | date-time |
curl -X PUT https://api.vitrinadev.com/api/v1/tickets/<id>/snooze \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"until": "2026-09-23T14:00:00.000Z"
}'Ejemplo de respuesta (200)
{
"data": [
{
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "snoozed",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000011",
"snoozed_until": "2026-09-23T14:00:00.000Z",
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": null,
"resolved_at": null,
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /tickets/{id}/stage
Move a ticket to another column
Moves the card. The move is rejected with a 400 when the current column's allowed_transitions_to does not name the target, or when the target belongs to another board — read them from GET /conversations/\{id\}/linked-records or GET /pipelines/\{id\}.
reason is recorded on the transition log beside the author. suggestion_id marks a Copilot stage suggestion as accepted, and is honoured only when that suggestion actually targets THIS ticket — a mismatched id is ignored rather than refused, so the move still lands.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
stageId | uuid | sí | |
reason | string | — | máx. 500 |
suggestion_id | uuid | — |
curl -X PUT https://api.vitrinadev.com/api/v1/tickets/<id>/stage \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"stageId": "55555555-0000-4000-8000-000000000012",
"reason": "El equipo ya tiene la boleta"
}'Ejemplo de respuesta (200)
{
"data": [
{
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "open",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000012",
"snoozed_until": null,
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": null,
"resolved_at": null,
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PUT /tickets/{id}/status
Set the ticket status
open · pending · snoozed · resolved · closed. snoozed_until is REQUIRED when status is snoozed and ignored otherwise — PUT /tickets/\{id\}/snooze is the same move with a body that cannot forget the time.
Setting resolved here does NOT fire ticket.resolved: that Evento is emitted by PUT /tickets/\{id\}/close and by the conversation cascade, which are the paths that also stamp resolved_by/resolved_at. Resolve through one of those if a receiver is watching.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | string | sí | mín. 1 |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
status | open \ | pending \ | snoozed \ |
snoozed_until | string | — | date-time |
curl -X PUT https://api.vitrinadev.com/api/v1/tickets/<id>/status \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "snoozed",
"snoozed_until": "2026-09-23T14:00:00.000Z"
}'Ejemplo de respuesta (200)
{
"data": [
{
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "snoozed",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000011",
"snoozed_until": "2026-09-23T14:00:00.000Z",
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": null,
"resolved_at": null,
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
]
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /tickets/stage/{stageId}
Tickets on one column of a board
The cards in a column, paged. This is how a board is read: one call per column, with the columns themselves coming from GET /pipelines/\{id\}. A stage of a non-ticket board simply has no tickets on it and answers an empty page.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
stageId | path | string | sí | mín. 1 |
page | query | integer | no | ≥ 1, por defecto 1 |
limit | query | integer | no | ≥ 1, ≤ 100, por defecto 20 |
sortBy | query | created_at \ | updated_at \ | status |
sortOrder | query | asc \ | desc | no |
curl https://api.vitrinadev.com/api/v1/tickets/stage/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "dddddddd-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_seq": 6,
"display_id": "T-6",
"status": "open",
"handler": "human",
"assignee_user_id": "11111111-0000-4000-8000-000000000001",
"assignee_ai_agent_graph_id": null,
"team_id": "cccccccc-0000-4000-8000-000000000001",
"contact_id": "22222222-0000-4000-8000-000000000001",
"lead_id": null,
"origin_conversation_id": "bbbbbbbb-0000-4000-8000-000000000001",
"current_stage_id": "55555555-0000-4000-8000-000000000011",
"snoozed_until": null,
"reason": "El cliente pide reagendar su visita",
"opened_by": "human",
"opened_via": "admin_ui",
"resolved_by": null,
"resolved_at": null,
"brand": null,
"created_at": "2026-09-22T11:06:47.738Z",
"updated_at": "2026-09-22T11:06:54.183Z"
}
],
"meta": {
"pagination": {
"total": 1,
"page": 1,
"limit": 20
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /tickets/status-counts
Ticket counts by lifecycle state
One number per state for the whole workspace, plus two cross-cuts: human (tickets a person is handling) and marketing. closing is the grace window between a resolve and the auto-close, so it is not one of the status values a ticket ever holds — it is a count of resolved tickets whose close timer is running.
curl https://api.vitrinadev.com/api/v1/tickets/status-counts \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"total": 5,
"open": 1,
"pending": 0,
"snoozed": 0,
"resolved": 0,
"closed": 4,
"closing": 0,
"human": 1,
"marketing": 0
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /tickets/today-counts
Ticket counts for tickets opened today
The same buckets as /tickets/status-counts, narrowed to tickets OPENED today in the workspace's timezone — the day's workload, not the backlog.
curl https://api.vitrinadev.com/api/v1/tickets/today-counts \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"total": 1,
"open": 1,
"pending": 0,
"snoozed": 0,
"resolved": 0,
"closed": 0,
"closing": 0,
"human": 1,
"marketing": 0
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
Ajustes del workspace
La identidad general del workspace (nombre, zona horaria, idioma, moneda, sitio web) y las decisiones de Vitrina Ads: cómo se respondió el paso del tag y la meta mensual.
Credenciales de herramientas
La bóveda detrás de una herramienta personalizada autenticada — el secreto nunca vuelve a mostrarse, solo un preview.