Cotizaciones
Una oferta de precio sobre una unidad, congelada al emitirse. No reserva nada: varios clientes pueden tener cotizaciones vigentes sobre el mismo auto.
Descarga la proyección completa de la API pública: openapi.json.
Vender una unidad explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /quotes | List cotizaciones |
POST | /quotes | Issue a cotización |
GET | /quotes/{id} | Fetch one cotización |
POST | /quotes/{id}/archive | Archive a cotización |
POST | /quotes/{id}/extend | Extend a cotización |
POST | /quotes/{id}/void | Void a cotización |
GET /quotes
List cotizaciones
The quote register, most recently issued first.
Everything each row says about the CAR is the frozen snapshot taken at issuance, not what the stock record says today — this endpoint never joins the vehicle.
The default page is the working register: archived documents are filed away and do not appear. Pass include_archived=true to see them (only the literal string true opts in).
VOIDED quotes ARE listed, with their folios. A voided cotización is never hidden, so "where is Q-47?" always has an answer (ADR 0058 §10) — there is deliberately no exclude_voided parameter. Each row carries a derived state: vigente | expirada | nula.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
include_archived | query | boolean | null | no | |
limit | query | integer | no | ≥ 1, ≤ 200 |
curl https://api.vitrinadev.com/api/v1/quotes \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"id": "d2d2d2d2-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "Q-47",
"display_seq": 47,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"list_price_clp": 12500000,
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"discount_pct": 4.8,
"unit_make": "Toyota",
"unit_model": "Corolla",
"unit_version": "XEI 2.0 CVT",
"unit_year": 2023,
"unit_plate": "RJKL48",
"unit_vin": "MMSDA31S6M0123457",
"unit_odometer_value": 18500,
"unit_odometer_unit": "km",
"unit_color": "Gris plata",
"discount_hidden": false,
"issued_at": "2026-09-08T14:02:11.000Z",
"expires_on": "2026-09-23",
"state": "vigente",
"voided_at": null,
"void_reason": null,
"voided_by": null,
"extended_at": null,
"extend_reason": null,
"extended_by": null,
"archived_at": null,
"archive_reason": null,
"archived_by": null,
"created_at": "2026-09-08T14:02:11.000Z",
"updated_at": "2026-09-08T14:02:11.000Z"
}
],
"meta": {
"total": 1
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /quotes
Issue a cotización
Issues a quote and allocates its folio (Q-1, Q-2, …) inside the inserting transaction, so a failed issue RELEASES the number rather than burning it.
A cotización is a SNAPSHOT, not a view. The vehicle’s asking price and its identity — marca, modelo, versión, año, patente, VIN, kilometraje, color — are copied onto the document by the database at this moment and are never re-read afterwards, so repricing or correcting the car does not change any quote already issued. Those fields are therefore NOT request parameters: a caller cannot record that a unit was listed at a price it never carried. Unknown body keys are a 400 for exactly that reason.
offered_price_clp is whole CLP pesos (bigint, never minor units, never decimals). The discount % is derived, never stored and never accepted as input — 100 × (list − offered) / list, clamped at 0 — which is safe precisely because both of its inputs are frozen on the row. tax_treatment is REQUIRED and has no default; it is not inherited from the unit’s régimen IVA, which can only say afecto/exento and cannot express pass_through.
buyer_contact_id is required — a price offer that names no offeree is not an offer. It and vehicle_id must belong to the calling workspace; a foreign id and a nonexistent one answer identically (404), deliberately, because a difference between them is an existence oracle. A soft-deleted or merged vehicle is also refused.
The offer stands through the whole of expires_on (default: 15 days from the issue date) and lapses the day after. A quote is explicitly not a reservation — it creates no hold on the unit, and several customers may hold quotes on the same car. Answers 201.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
vehicle_id | uuid | sí | |
buyer_contact_id | uuid | sí | |
salesperson_id | string | null | — | |
offered_price_clp | integer | sí | ≥ 0, ≤ 999999999999 |
tax_treatment | afecto \ | exento \ | no_gravado \ |
discount_hidden | boolean | — | |
issued_at | string | null | — | date-time |
expires_on | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
curl -X POST https://api.vitrinadev.com/api/v1/quotes \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"expires_on": "2026-09-23"
}'Ejemplo de respuesta (201)
{
"data": {
"id": "d2d2d2d2-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "Q-47",
"display_seq": 47,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"list_price_clp": 12500000,
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"discount_pct": 4.8,
"unit_make": "Toyota",
"unit_model": "Corolla",
"unit_version": "XEI 2.0 CVT",
"unit_year": 2023,
"unit_plate": "RJKL48",
"unit_vin": "MMSDA31S6M0123457",
"unit_odometer_value": 18500,
"unit_odometer_unit": "km",
"unit_color": "Gris plata",
"discount_hidden": false,
"issued_at": "2026-09-08T14:02:11.000Z",
"expires_on": "2026-09-23",
"state": "vigente",
"voided_at": null,
"void_reason": null,
"voided_by": null,
"extended_at": null,
"extend_reason": null,
"extended_by": null,
"archived_at": null,
"archive_reason": null,
"archived_by": null,
"created_at": "2026-09-08T14:02:11.000Z",
"updated_at": "2026-09-08T14:02:11.000Z"
}
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /quotes/{id}
Fetch one cotización
Accepts the UUID or the folio (Q-47) — the folio is the form the customer is holding. Only the Q- series resolves here; another entity’s display id (T-1247) is not a quote.
Everything returned about the CAR is the frozen snapshot as at issuance, not the stock record’s current values. state is DERIVED from two frozen columns plus the clock and is one of vigente | expirada | nula: a quote is still vigente throughout its expires_on day, and a void outranks an expiry (a voided quote reads nula even after its date has passed). There is no stored status column, so the state can never drift from the facts it comes from.
A quote belonging to another workspace is a 404, indistinguishable from one that does not exist.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
curl https://api.vitrinadev.com/api/v1/quotes/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"id": "d2d2d2d2-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "Q-47",
"display_seq": 47,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"list_price_clp": 12500000,
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"discount_pct": 4.8,
"unit_make": "Toyota",
"unit_model": "Corolla",
"unit_version": "XEI 2.0 CVT",
"unit_year": 2023,
"unit_plate": "RJKL48",
"unit_vin": "MMSDA31S6M0123457",
"unit_odometer_value": 18500,
"unit_odometer_unit": "km",
"unit_color": "Gris plata",
"discount_hidden": false,
"issued_at": "2026-09-08T14:02:11.000Z",
"expires_on": "2026-09-23",
"state": "vigente",
"voided_at": null,
"void_reason": null,
"voided_by": null,
"extended_at": null,
"extend_reason": null,
"extended_by": null,
"archived_at": null,
"archive_reason": null,
"archived_by": null,
"created_at": "2026-09-08T14:02:11.000Z",
"updated_at": "2026-09-08T14:02:11.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /quotes/{id}/archive
Archive a cotización
Files the document away from the working register, recording who did it and why.
This is NOT a delete and NOT a state. The folio survives, GET /quotes/\{id\} still resolves the document, and its state stays vigente | expirada | nula — archive is invisible to that derivation, so the three buckets stay disjoint and exhaustive (BR-217/218). The only effect is that GET /quotes stops returning it unless you pass include_archived=true.
A voided or expired quote CAN be archived — filing a dead document away is the ordinary case, and it is the difference from /extend, which a void forbids. The first archive stands: re-archiving is a 409 rather than an overwrite of the original actor and reason.
archive_reason is mandatory and must contain VISIBLE text. The actor comes from the authenticated principal, and this body is STRICT — a body-supplied archived_by is a 400. Requires quotes:write: filing a document away withdraws nothing and is strictly weaker than a void.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
archive_reason | string | sí | máx. 2000 |
curl -X POST https://api.vitrinadev.com/api/v1/quotes/<id>/archive \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"archive_reason": "cotización antigua, ya no vigente en el seguimiento"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "d2d2d2d2-0000-4000-8000-000000000003",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "Q-31",
"display_seq": 31,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"list_price_clp": 12500000,
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"discount_pct": 4.8,
"unit_make": "Toyota",
"unit_model": "Corolla",
"unit_version": "XEI 2.0 CVT",
"unit_year": 2023,
"unit_plate": "RJKL48",
"unit_vin": "MMSDA31S6M0123457",
"unit_odometer_value": 18500,
"unit_odometer_unit": "km",
"unit_color": "Gris plata",
"discount_hidden": false,
"issued_at": "2026-09-08T14:02:11.000Z",
"expires_on": "2026-08-01",
"state": "expirada",
"voided_at": null,
"void_reason": null,
"voided_by": null,
"extended_at": null,
"extend_reason": null,
"extended_by": null,
"archived_at": "2026-09-10T08:00:00.000Z",
"archive_reason": "cotización antigua, ya no vigente en el seguimiento",
"archived_by": "11111111-0000-4000-8000-000000000001",
"created_at": "2026-09-08T14:02:11.000Z",
"updated_at": "2026-09-08T14:02:11.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /quotes/{id}/extend
Extend a cotización
Moves the validity window to a new expires_on, recording who did it and why. This is the ONLY change an issued quote accepts (BR-221): the price, the car, the buyer and everything else the document says are frozen.
Permitted on a vigente quote and on an expirada one — extending an expired quote returns it to vigente, which is the capability's whole purpose (the customer's financing took longer than the 15-day window). Refused on a nula one with a 409: the void is terminal (BR-222), and the table's own trigger refuses it too.
extend_reason is MANDATORY and must contain VISIBLE text. Extending is economically identical to re-issuing, so it is attributable on the same terms — un-attributed, it would be the one way to move a price commitment with no name on it (BR-224). The database enforces this: expires_on may only move in a statement that also stamps a fresh extended_at / extend_reason / extended_by, so an unattributed extension is unstorable rather than merely discouraged.
The new date may be any real calendar day, in either direction and with no bound (BR-214 as recovered) — but it must DIFFER from the date the document already carries, since an extension that moves nothing would record an act that never happened (400).
The actor comes from the authenticated principal. Unlike /void, this body is STRICT: a body-supplied extended_by is a 400, not a silently ignored key. The row carries the LAST extension; every extension also writes a quotes.extend audit row, which is the full history. Requires quotes:write.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
expires_on | string | sí | patrón ^\d{4}-\d{2}-\d{2}$ |
extend_reason | string | sí | máx. 2000 |
curl -X POST https://api.vitrinadev.com/api/v1/quotes/<id>/extend \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"expires_on": "2026-10-20",
"extend_reason": "el cliente sigue esperando la aprobación del crédito"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "d2d2d2d2-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "Q-47",
"display_seq": 47,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"list_price_clp": 12500000,
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"discount_pct": 4.8,
"unit_make": "Toyota",
"unit_model": "Corolla",
"unit_version": "XEI 2.0 CVT",
"unit_year": 2023,
"unit_plate": "RJKL48",
"unit_vin": "MMSDA31S6M0123457",
"unit_odometer_value": 18500,
"unit_odometer_unit": "km",
"unit_color": "Gris plata",
"discount_hidden": false,
"issued_at": "2026-09-08T14:02:11.000Z",
"expires_on": "2026-10-20",
"state": "vigente",
"voided_at": null,
"void_reason": null,
"voided_by": null,
"extended_at": "2026-09-20T09:11:44.000Z",
"extend_reason": "el cliente sigue esperando la aprobación del crédito",
"extended_by": "11111111-0000-4000-8000-000000000001",
"archived_at": null,
"archive_reason": null,
"archived_by": null,
"created_at": "2026-09-08T14:02:11.000Z",
"updated_at": "2026-09-08T14:02:11.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /quotes/{id}/void
Void a cotización
Voids the quote. It is never deleted and it KEEPS ITS FOLIO (ADR 0058 §10): the row records the time, the reason and the acting principal and remains readable. There is deliberately no DELETE verb on this resource — and no PUT/PATCH either, because the issued document is immutable. The correction path for anything the document says is void + re-issue, which is also the only path that leaves a trail.
The void is terminal: a voided quote can never be restored, extended or re-voided, so a second attempt is a 409 and the first actor and reason stand. An EXPIRED quote can still be voided.
void_reason is mandatory and must contain VISIBLE text — a reason made only of whitespace, a U+00A0 or a zero-width space is refused at the edge and by a table CHECK, because "why did we cancel 40 quotes this month" is answerable only if the field is populated.
The actor comes from the authenticated principal; a voided_by in the body is IGNORED rather than rejected — a self-declared actor is not an attestation. Requires quotes:void, a DIFFERENT authority from quotes:write.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
void_reason | string | sí | máx. 2000 |
curl -X POST https://api.vitrinadev.com/api/v1/quotes/<id>/void \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"void_reason": "el cliente desistió de la compra"
}'Ejemplo de respuesta (200)
{
"data": {
"id": "d2d2d2d2-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "Q-52",
"display_seq": 52,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000002",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"list_price_clp": 12500000,
"offered_price_clp": 11900000,
"tax_treatment": "afecto",
"discount_pct": 4.8,
"unit_make": "Toyota",
"unit_model": "Corolla",
"unit_version": null,
"unit_year": 2023,
"unit_plate": null,
"unit_vin": null,
"unit_odometer_value": 18500,
"unit_odometer_unit": "km",
"unit_color": "Gris plata",
"discount_hidden": false,
"issued_at": "2026-09-08T14:02:11.000Z",
"expires_on": "2026-09-23",
"state": "nula",
"voided_at": "2026-09-15T11:20:03.000Z",
"void_reason": "el cliente desistió de la compra",
"voided_by": "11111111-0000-4000-8000-000000000001",
"extended_at": null,
"extend_reason": null,
"extended_by": null,
"archived_at": null,
"archive_reason": null,
"archived_by": null,
"created_at": "2026-09-08T14:02:11.000Z",
"updated_at": "2026-09-08T14:02:11.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429