VitrinaAPI

Tickets

El caso que sobrevive al hilo: puede dueñar varias conversaciones, vive en una columna de un tablero y tiene responsable y SLA.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

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étodoRutaQué hace
GET/tickets/{id}Get ticket
PUT/tickets/{id}/assignAssign a ticket to a teammate or a team
POST/tickets/{id}/claimClaim a ticket for the caller
PUT/tickets/{id}/closeResolve a ticket (the manual "done" action)
GET/tickets/{id}/conversationsThe conversations this ticket owns
GET/tickets/{id}/messagesOne timeline across every conversation on this ticket
PUT/tickets/{id}/snoozeSnooze a ticket until a wake-up time
PUT/tickets/{id}/stageMove a ticket to another column
PUT/tickets/{id}/statusSet the ticket status
GET/tickets/stage/{stageId}Tickets on one column of a board
GET/tickets/status-countsTicket counts by lifecycle state
GET/tickets/today-countsTicket 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ámetroEnTipoObligatorioRestricciones
idpathstringmí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ámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
handlerbot \human \external
assignee_user_idstring | null
team_idstring | 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ámetroEnTipoObligatorioRestricciones
idpathstringmí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ámetroEnTipoObligatorioRestricciones
idpathstringmí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ámetroEnTipoObligatorioRestricciones
idpathstringmí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ámetroEnTipoObligatorioRestricciones
idpathstringmín. 1
limitqueryintegerno≥ 1, ≤ 2000, por defecto 500
beforequerystringnodate-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ámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
untilstringdate-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ámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
stageIduuid
reasonstringmáx. 500
suggestion_iduuid
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ámetroEnTipoObligatorioRestricciones
idpathstringmín. 1

Cuerpo

CampoTipoObligatorioRestricciones
statusopen \pending \snoozed \
snoozed_untilstringdate-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ámetroEnTipoObligatorioRestricciones
stageIdpathstringmín. 1
pagequeryintegerno≥ 1, por defecto 1
limitqueryintegerno≥ 1, ≤ 100, por defecto 20
sortByquerycreated_at \updated_at \status
sortOrderqueryasc \descno
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

En esta página