VitrinaAPI

Tickets

The case that outlives the thread: it can own several conversations, lives on a column of a board, and has an owner and an SLA.

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/tickets/{id}Get ticket
PUT/tickets/{id}/assignAssign a ticket to a teammate or a team
GET/tickets/{id}/assignment-eventsEvery contratista step on a caso (timeline)
GET/tickets/{id}/assignmentsThe contratistas a caso was sent to
POST/tickets/{id}/assignmentsAssign a caso (or some of its observaciones) to a contratista
GET/tickets/{id}/caseA post-venta caso: unidad, observaciones, fotos, visita, encuesta
PATCH/tickets/{id}/caseSet a caso’s priority or unidad
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
POST/tickets/{id}/mediaAttach photos or videos to a caso
GET/tickets/{id}/messagesOne timeline across every conversation on this ticket
POST/tickets/{id}/observationsAdd an observación (a defect) to a caso
PATCH/tickets/{id}/observations/{observationId}Correct or resolve an observación
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
POST/tickets/{id}/visitSchedule the technical visit of a caso
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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl https://api.vitrinadev.com/api/v1/tickets/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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"
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1

Body

FieldTypeRequiredConstraints
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"
  }'

Example response (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"
    }
  ]
}

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

GET /tickets/{id}/assignment-events

Every contratista step on a caso (timeline)

assigned, notified, viewed, accepted, declined, visit_scheduled, media_added, done, cancelled — newest first, with who did it (staff, contratista, system). The same steps also appear as system lines in the caso’s conversation thread. Requires tickets:read. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl https://api.vitrinadev.com/api/v1/tickets/<id>/assignment-events \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "0199e3a0-0000-7000-8000-000000000a31",
      "assignment_id": "0199e3a0-0000-7000-8000-000000000a21",
      "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
      "kind": "accepted",
      "actor": "contratista",
      "data": {},
      "created_at": "2026-10-20T13:00:00.000Z"
    }
  ]
}

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

GET /tickets/{id}/assignments

The contratistas a caso was sent to

Newest first, with the contratista’s name, trade and the observaciones sent. Requires tickets:read. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl https://api.vitrinadev.com/api/v1/tickets/<id>/assignments \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "0199e3a0-0000-7000-8000-000000000a21",
      "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
      "contratista_id": "0199e3a0-0000-7000-8000-000000000a11",
      "status": "assigned",
      "brief": "Revisar la filtración detrás del WC.",
      "owner_window": "Lunes a viernes de 9 a 18",
      "appointment_id": null,
      "visit_starts_at": null,
      "notified_at": "2026-10-20T12:01:00.000Z",
      "notify_error": null,
      "accepted_at": null,
      "scheduled_at": null,
      "done_at": null,
      "declined_at": null,
      "decline_reason": null,
      "completion_notes": null,
      "created_at": "2026-10-20T12:00:00.000Z",
      "contratista_name": "Gasfitería Araucanía",
      "contratista_trade": "gasfitería",
      "contratista_whatsapp": "+56912345678",
      "observation_ids": [
        "0199e3a0-0000-7000-8000-000000000b01"
      ]
    }
  ]
}

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

POST /tickets/{id}/assignments

Assign a caso (or some of its observaciones) to a contratista

Sends the caso to a contratista: they get a WhatsApp with a no-login link (/caso/&lt;token&gt;, 30 days) where they accept or decline, schedule the visit, upload before / after photos and close. Without observation_ids every open observación not already sent goes; an observación already with another live contratista is a 409. Assigning the same contratista again reuses the live assignment. The result says whether the WhatsApp went out (notified); a closed 24 h window or a missing number comes back in notify_error, and the link can be copied instead. Requires tickets:write. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X POST https://api.vitrinadev.com/api/v1/tickets/<id>/assignments \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contratista_id": "0199e3a0-0000-7000-8000-000000000a11",
    "brief": "Revisar la filtración detrás del WC.",
    "owner_window": "Lunes a viernes de 9 a 18"
  }'

Example response (201)

{
  "data": {
    "assignment": {
      "id": "0199e3a0-0000-7000-8000-000000000a21",
      "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
      "contratista_id": "0199e3a0-0000-7000-8000-000000000a11",
      "status": "assigned",
      "brief": "Revisar la filtración detrás del WC.",
      "owner_window": "Lunes a viernes de 9 a 18",
      "appointment_id": null,
      "visit_starts_at": null,
      "notified_at": "2026-10-20T12:01:00.000Z",
      "notify_error": null,
      "accepted_at": null,
      "scheduled_at": null,
      "done_at": null,
      "declined_at": null,
      "decline_reason": null,
      "completion_notes": null,
      "created_at": "2026-10-20T12:00:00.000Z",
      "contratista_name": "Gasfitería Araucanía",
      "contratista_trade": "gasfitería",
      "contratista_whatsapp": "+56912345678",
      "observation_ids": [
        "0199e3a0-0000-7000-8000-000000000b01"
      ]
    },
    "link": "https://app.example.com/caso/eyJhbGciOi…",
    "notified": true,
    "notify_error": null,
    "observation_ids": [
      "0199e3a0-0000-7000-8000-000000000b01"
    ]
  }
}

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

GET /tickets/{id}/case

A post-venta caso: unidad, observaciones, fotos, visita, encuesta

The ticket seen as a caso de post-venta: its proyecto and unidad, every observación with its garantía (computed from the dates on record), the owner’s photos and videos (copied in from the caso’s conversations on read) and staff uploads, the next technical visita and the owner’s own 1 to 5 rating. \{id\} is the ticket uuid or its T-number. Requires tickets:read. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl https://api.vitrinadev.com/api/v1/tickets/<id>/case \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
    "display_id": "T-5",
    "priority": "alta",
    "project": {
      "id": "0199e3a0-0000-7000-8000-000000000101",
      "name": "Vista Golf Departamentos"
    },
    "unit": {
      "id": "0199e3a0-0000-7000-8000-000000000401",
      "display_id": "I-42",
      "unit_number": "302",
      "kind": "departamento"
    },
    "observations": [
      {
        "id": "0199e3a0-0000-7000-8000-000000000b01",
        "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
        "partida": "instalaciones_sanitarias",
        "description": "Filtración detrás del WC del baño principal, hace dos meses.",
        "warranty_years": 5,
        "warranty_base": "dom",
        "warranty_start_at": "2024-03-15",
        "warranty_expires_at": "2029-03-15",
        "warranty_status": "dentro_de_plazo",
        "needs_review": false,
        "status": "open",
        "created_by": null,
        "created_at": "2026-10-20T12:00:00.000Z",
        "resolved_at": null
      }
    ],
    "media": [
      {
        "id": "0199e3a0-0000-7000-8000-000000000f01",
        "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
        "observation_id": null,
        "url": "https://media.example.com/conversations/abc/filtracion.jpg",
        "media_type": "image",
        "source": "conversation",
        "message_id": null,
        "caption": null,
        "created_at": "2026-10-20T12:01:00.000Z"
      }
    ],
    "next_visit": {
      "appointment_id": "0199e3a0-0000-7000-8000-000000000e01",
      "starts_at": "2026-10-22T13:00:00.000Z",
      "ends_at": "2026-10-22T14:30:00.000Z",
      "status": "confirmed"
    },
    "survey": {
      "asked_at": null,
      "score": null
    }
  }
}

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

PATCH /tickets/{id}/case

Set a caso’s priority or unidad

Staff set the priority (emergencia pushes the proyecto’s on-call people at once, bell + push) and/or the unidad. The unidad must be one the ticket’s contact owns (unit_owner), else 400; the proyecto follows from it. Requires tickets:write. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X PATCH https://api.vitrinadev.com/api/v1/tickets/<id>/case \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "priority": "alta",
    "property_id": "0199e3a0-0000-7000-8000-000000000401"
  }'

Example response (200)

{
  "data": {
    "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
    "display_id": "T-5",
    "priority": "alta",
    "project": {
      "id": "0199e3a0-0000-7000-8000-000000000101",
      "name": "Vista Golf Departamentos"
    },
    "unit": {
      "id": "0199e3a0-0000-7000-8000-000000000401",
      "display_id": "I-42",
      "unit_number": "302",
      "kind": "departamento"
    },
    "observations": [
      {
        "id": "0199e3a0-0000-7000-8000-000000000b01",
        "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
        "partida": "instalaciones_sanitarias",
        "description": "Filtración detrás del WC del baño principal, hace dos meses.",
        "warranty_years": 5,
        "warranty_base": "dom",
        "warranty_start_at": "2024-03-15",
        "warranty_expires_at": "2029-03-15",
        "warranty_status": "dentro_de_plazo",
        "needs_review": false,
        "status": "open",
        "created_by": null,
        "created_at": "2026-10-20T12:00:00.000Z",
        "resolved_at": null
      }
    ],
    "media": [
      {
        "id": "0199e3a0-0000-7000-8000-000000000f01",
        "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
        "observation_id": null,
        "url": "https://media.example.com/conversations/abc/filtracion.jpg",
        "media_type": "image",
        "source": "conversation",
        "message_id": null,
        "caption": null,
        "created_at": "2026-10-20T12:01:00.000Z"
      }
    ],
    "next_visit": {
      "appointment_id": "0199e3a0-0000-7000-8000-000000000e01",
      "starts_at": "2026-10-22T13:00:00.000Z",
      "ends_at": "2026-10-22T14:30:00.000Z",
      "status": "confirmed"
    },
    "survey": {
      "asked_at": null,
      "score": null
    }
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X POST https://api.vitrinadev.com/api/v1/tickets/<id>/claim \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "ticketId": "dddddddd-0000-4000-8000-000000000001",
    "assigneeUserId": "11111111-0000-4000-8000-000000000001",
    "alreadyClaimed": false
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X PUT https://api.vitrinadev.com/api/v1/tickets/<id>/close \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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"
    }
  ]
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl https://api.vitrinadev.com/api/v1/tickets/<id>/conversations \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (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"
  }
}

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

POST /tickets/{id}/media

Attach photos or videos to a caso

Staff uploads (source: staff), optionally on one observación. Upload the file with POST /tenant/media first and send its url. A url already on the caso is left as it is. The owner’s own WhatsApp media attach on their own. Requires tickets:write. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X POST https://api.vitrinadev.com/api/v1/tickets/<id>/media \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      {
        "url": "https://media.example.com/t/other/visita-1.jpg",
        "observation_id": "0199e3a0-0000-7000-8000-000000000b01",
        "caption": "Después de la reparación"
      }
    ]
  }'

Example response (201)

{
  "data": {
    "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
    "display_id": "T-5",
    "priority": "alta",
    "project": {
      "id": "0199e3a0-0000-7000-8000-000000000101",
      "name": "Vista Golf Departamentos"
    },
    "unit": {
      "id": "0199e3a0-0000-7000-8000-000000000401",
      "display_id": "I-42",
      "unit_number": "302",
      "kind": "departamento"
    },
    "observations": [
      {
        "id": "0199e3a0-0000-7000-8000-000000000b01",
        "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
        "partida": "instalaciones_sanitarias",
        "description": "Filtración detrás del WC del baño principal, hace dos meses.",
        "warranty_years": 5,
        "warranty_base": "dom",
        "warranty_start_at": "2024-03-15",
        "warranty_expires_at": "2029-03-15",
        "warranty_status": "dentro_de_plazo",
        "needs_review": false,
        "status": "open",
        "created_by": null,
        "created_at": "2026-10-20T12:00:00.000Z",
        "resolved_at": null
      }
    ],
    "media": [
      {
        "id": "0199e3a0-0000-7000-8000-000000000f01",
        "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
        "observation_id": null,
        "url": "https://media.example.com/conversations/abc/filtracion.jpg",
        "media_type": "image",
        "source": "conversation",
        "message_id": null,
        "caption": null,
        "created_at": "2026-10-20T12:01:00.000Z"
      }
    ],
    "next_visit": {
      "appointment_id": "0199e3a0-0000-7000-8000-000000000e01",
      "starts_at": "2026-10-22T13:00:00.000Z",
      "ends_at": "2026-10-22T14:30:00.000Z",
      "status": "confirmed"
    },
    "survey": {
      "asked_at": null,
      "score": null
    }
  }
}

Answers: 201 · 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.

ParameterInTypeRequiredConstraints
idpathstringyesmí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"

Example response (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
  }
}

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

POST /tickets/{id}/observations

Add an observación (a defect) to a caso

One conversation can report several defects, so one caso holds several observaciones, each with its own partida and garantía. The plazo comes from the law (terminaciones 3 años from the unidad’s inscripción en el CBR; instalaciones and techumbre 5, estructura 10, from the proyecto’s recepción definitiva) and the dates on record: dentro_de_plazo, fuera_de_plazo or por_revisar (a date is missing). An out-of-plazo defect is still registered and flagged needs_review. 400 when the same partida and description are already on the caso. Requires tickets:write. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X POST https://api.vitrinadev.com/api/v1/tickets/<id>/observations \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "partida": "instalaciones_sanitarias",
    "description": "Filtración detrás del WC del baño principal."
  }'

Example response (201)

{
  "data": {
    "id": "0199e3a0-0000-7000-8000-000000000b01",
    "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
    "partida": "instalaciones_sanitarias",
    "description": "Filtración detrás del WC del baño principal, hace dos meses.",
    "warranty_years": 5,
    "warranty_base": "dom",
    "warranty_start_at": "2024-03-15",
    "warranty_expires_at": "2029-03-15",
    "warranty_status": "dentro_de_plazo",
    "needs_review": false,
    "status": "open",
    "created_by": null,
    "created_at": "2026-10-20T12:00:00.000Z",
    "resolved_at": null
  }
}

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

PATCH /tickets/{id}/observations/{observationId}

Correct or resolve an observación

Changing the partida or the years recomputes the garantía. status: resolved stamps the resolution time. Requires tickets:write. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
observationIdpathuuidyes
curl -X PATCH https://api.vitrinadev.com/api/v1/tickets/<id>/observations/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "resolved"
  }'

Example response (200)

{
  "data": {
    "id": "0199e3a0-0000-7000-8000-000000000b01",
    "ticket_id": "0199e3a0-0000-7000-8000-000000000a01",
    "partida": "instalaciones_sanitarias",
    "description": "Filtración detrás del WC del baño principal, hace dos meses.",
    "warranty_years": 5,
    "warranty_base": "dom",
    "warranty_start_at": "2024-03-15",
    "warranty_expires_at": "2029-03-15",
    "warranty_status": "dentro_de_plazo",
    "needs_review": false,
    "status": "resolved",
    "created_by": null,
    "created_at": "2026-10-20T12:00:00.000Z",
    "resolved_at": null
  }
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1

Body

FieldTypeRequiredConstraints
untilstringyesdate-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"
  }'

Example response (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"
    }
  ]
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1

Body

FieldTypeRequiredConstraints
stageIduuidyes
reasonstring—má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"
  }'

Example response (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"
    }
  ]
}

Answers: 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.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1

Body

FieldTypeRequiredConstraints
statusopen \pending \snoozed \
snoozed_untilstring—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"
  }'

Example response (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"
    }
  ]
}

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

POST /tickets/{id}/visit

Schedule the technical visit of a caso

Books the visita as an appointment linked to the caso and tells the owner on the caso’s conversation (unless notify_owner is false). One live visit per caso: scheduling again cancels the previous one. The agent answers «¿cuándo viene el técnico?» from it. Requires tickets:write. Only on a workspace that runs the Proyectos module (real-estate vertical, inmobiliaria); any other answers 403 MODULE_NOT_ENABLED.

ParameterInTypeRequiredConstraints
idpathstringyesmín. 1
curl -X POST https://api.vitrinadev.com/api/v1/tickets/<id>/visit \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "starts_at": "2026-10-22T13:00:00.000Z",
    "ends_at": "2026-10-22T14:30:00.000Z"
  }'

Example response (201)

{
  "data": {
    "appointment_id": "0199e3a0-0000-7000-8000-000000000e01",
    "starts_at": "2026-10-22T13:00:00.000Z",
    "ends_at": "2026-10-22T14:30:00.000Z"
  }
}

Answers: 201 · 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.

ParameterInTypeRequiredConstraints
stageIdpathstringyesmí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"

Example response (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
    }
  }
}

Answers: 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"

Example response (200)

{
  "data": {
    "total": 5,
    "open": 1,
    "pending": 0,
    "snoozed": 0,
    "resolved": 0,
    "closed": 4,
    "closing": 0,
    "human": 1,
    "marketing": 0
  }
}

Answers: 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"

Example response (200)

{
  "data": {
    "total": 1,
    "open": 1,
    "pending": 0,
    "snoozed": 0,
    "resolved": 0,
    "closed": 0,
    "closing": 0,
    "human": 1,
    "marketing": 0
  }
}

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

On this page