Reservations
The only one of these eight that locks inventory: takes a hold on a unit, with an optional deposit and a term.
Download the full API pública projection: openapi.json.
Sell a unit explains this resource in prose, with runnable examples.
| Method | Path | What it does |
|---|---|---|
GET | /reservations | List reservations — the active-holds worklist |
POST | /reservations | Take a reservation |
GET | /reservations/{id} | Fetch one reservation, with its payments and totals |
POST | /reservations/{id}/abono-disposition | Dispose of the abono on a reservation |
POST | /reservations/{id}/convert | Convert a reservation into a nota de venta |
POST | /reservations/{id}/void | Void a reservation |
GET /reservations
List reservations — the active-holds worklist
The dealership’s HOLD REGISTER, most urgent first: ordered by hold_expires_on ascending, then by folio descending. Every status is included by default — a voided or completed reserva keeps its folio and stays listable so "where is reserva R-47?" has an answer (ADR 0058 §10) — so pass status to narrow to the live ones.
Each row carries its money. total_pagos_clp is the sum of every ledger payment (customer_payment allocated via payment_allocation) recorded against the hold and payment_count is how many there are; a hold with no payments reports 0 / 0. Both come from one grouped join, so the list is one query rather than one per row.
The saldo is NOT returned, deliberately. It is agreed_price_clp - total_pagos_clp, and agreed_price_clp is nullable (null = "not recorded", which is not the same claim as 0). Compute it client-side and render "—" when the price is absent; a server-side number would have to invent one of the two.
salesperson_id is BR-312’s filter — "which holds are mine?". vehicle_id returns a unit’s hold HISTORY and answers nothing about whether that unit is available: vehicle.status via /vehicles is the only availability truth (ADR 0030).
Unknown query parameters are a 400, not a silent drop: a filter the caller believes is being applied, returning a full unfiltered list with a 200, is a worse answer than a refusal.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
status | query | activa \ | completada \ | archivada \ |
salesperson_id | query | uuid | no | |
vehicle_id | query | uuid | no | |
holder_contact_id | query | uuid | no | |
limit | query | integer | no | ≥ 1, ≤ 200 |
curl https://api.vitrinadev.com/api/v1/reservations \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"id": "d3d3d3d3-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "R-18",
"display_seq": 18,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"abono_expected_clp": 300000,
"taken_at": "2026-09-18T16:40:00.000Z",
"hold_expires_on": "2026-09-25",
"keep_advertised": true,
"status": "activa",
"abono_disposition": null,
"abono_disposition_reason": null,
"abono_disposition_at": null,
"abono_disposition_by": null,
"voided_at": null,
"void_reason": null,
"voided_by": null,
"created_at": "2026-09-18T16:40:00.000Z",
"updated_at": "2026-09-18T16:40:00.000Z"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /reservations
Take a reservation
Takes a hold on a unit — the ONLY thing on this wave that locks inventory (a cotización creates no hold). Born activa; there is no draft state and no status field to supply.
vehicle_id and holder_contact_id are required and 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.
hold_expires_on is REQUIRED and has no default (BR-241, corrected): unlike a cotización, a reserva locks the unit and can take a non-refundable deposit, so every hold states its own term rather than inheriting a silent policy number. agreed_price_clp, omitted, copies the vehicle's current asking price (BR-242) — that is a real default, not zero. keep_advertised, omitted, keeps the column default true (the marketplace ads stay live); an explicit false is the per-reserva opt-out that retracts them through the existing cierre queue, and it is set-once — there is no endpoint to change it on a standing hold.
Exactly one ACTIVE hold per unit. A second take on a unit that already has one is a 409, enforced by a partial unique index so a race loses too. Unknown body keys are a 400. Answers 201.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
vehicle_id | uuid | yes | |
holder_contact_id | uuid | yes | |
salesperson_id | string | null | — | |
agreed_price_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
taken_at | string | null | — | date-time |
hold_expires_on | string | yes | patrón ^\d{4}-\d{2}-\d{2}$ |
keep_advertised | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/reservations \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"hold_expires_on": "2026-09-25",
"keep_advertised": true
}'Example response (201)
{
"data": {
"id": "d3d3d3d3-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "R-18",
"display_seq": 18,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"abono_expected_clp": 300000,
"taken_at": "2026-09-18T16:40:00.000Z",
"hold_expires_on": "2026-09-25",
"keep_advertised": true,
"status": "activa",
"abono_disposition": null,
"abono_disposition_reason": null,
"abono_disposition_at": null,
"abono_disposition_by": null,
"voided_at": null,
"void_reason": null,
"voided_by": null,
"created_at": "2026-09-18T16:40:00.000Z",
"updated_at": "2026-09-18T16:40:00.000Z"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /reservations/{id}
Fetch one reservation, with its payments and totals
Accepts the UUID or the folio (R-12).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | any | yes |
curl https://api.vitrinadev.com/api/v1/reservations/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"id": "d3d3d3d3-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "R-18",
"display_seq": 18,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"abono_expected_clp": 300000,
"taken_at": "2026-09-18T16:40:00.000Z",
"hold_expires_on": "2026-09-25",
"keep_advertised": true,
"status": "activa",
"abono_disposition": null,
"abono_disposition_reason": null,
"abono_disposition_at": null,
"abono_disposition_by": null,
"voided_at": null,
"void_reason": null,
"voided_by": null,
"created_at": "2026-09-18T16:40:00.000Z",
"updated_at": "2026-09-18T16:40:00.000Z",
"payments": [
{
"id": "d6d6d6d6-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"reservation_id": "d3d3d3d3-0000-4000-8000-000000000001",
"sale_note_id": null,
"carried_from_reservation_id": null,
"instrument": "transferencia",
"amount_clp": 300000,
"paid_on": "2026-09-18",
"bank": "Banco Estado",
"account_number": null,
"document_number": null,
"note": "abono al tomar la reserva",
"card_fee_bps": null,
"card_surcharge_clp": null,
"card_surcharge_source": null,
"created_at": "2026-09-18T16:41:02.000Z"
}
],
"totals": {
"total_a_pagar_clp": 300000,
"total_pagos_clp": 300000,
"saldo_clp": 0,
"recargo_tarjeta_clp": 0,
"total_a_cobrar_clp": 300000,
"payment_count": 1,
"complete": true,
"gaps": []
},
"converted_to_sale_note": null
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /reservations/{id}/abono-disposition
Dispose of the abono on a reservation
A FOURTH distinct scope, reservations:dispose_abono, not OR’d with :write/:void. May also close a still-activa hold in the same act.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | any | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
abono_disposition | aplicado \ | devuelto \ | perdido |
abono_disposition_reason | string | yes | máx. 2000 |
void_reason | string | null | — | máx. 2000 |
curl -X POST https://api.vitrinadev.com/api/v1/reservations/<id>/abono-disposition \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"abono_disposition": "devuelto",
"abono_disposition_reason": "el cliente desistió; se devolvió el abono"
}'Example response (200)
{
"data": {
"id": "d3d3d3d3-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "R-18",
"display_seq": 18,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"abono_expected_clp": 300000,
"taken_at": "2026-09-18T16:40:00.000Z",
"hold_expires_on": "2026-09-25",
"keep_advertised": true,
"status": "anulada",
"abono_disposition": "devuelto",
"abono_disposition_reason": "el cliente desistió; se devolvió el abono",
"abono_disposition_at": "2026-09-21T09:00:00.000Z",
"abono_disposition_by": "11111111-0000-4000-8000-000000000001",
"voided_at": "2026-09-21T09:00:00.000Z",
"void_reason": "cierre por disposición del abono",
"voided_by": "11111111-0000-4000-8000-000000000001",
"created_at": "2026-09-18T16:40:00.000Z",
"updated_at": "2026-09-18T16:40:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /reservations/{id}/convert
Convert a reservation into a nota de venta
Produces a NEW sale-note document — 201, the same as POST /sale-notes. Scoped sale_notes:write, borrowed rather than a fifth reservations scope.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | any | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
seller_of_record | consignante \ | retoma \ | automotora |
tax_treatment | afecto \ | exento \ | no_gravado \ |
tax_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
issued_at | string | null | — | date-time |
lead_id | any | — |
curl -X POST https://api.vitrinadev.com/api/v1/reservations/<id>/convert \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"seller_of_record": "automotora",
"tax_treatment": "afecto",
"tax_clp": 1900000,
"issued_at": "2026-09-24T12:00:00.000Z"
}'Example response (201)
{
"data": {
"reservation": {
"id": "d3d3d3d3-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "R-18",
"display_seq": 18,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"abono_expected_clp": 300000,
"taken_at": "2026-09-18T16:40:00.000Z",
"hold_expires_on": "2026-09-25",
"keep_advertised": true,
"status": "completada",
"abono_disposition": "aplicado",
"abono_disposition_reason": null,
"abono_disposition_at": "2026-09-24T12:00:00.000Z",
"abono_disposition_by": "11111111-0000-4000-8000-000000000001",
"voided_at": null,
"void_reason": null,
"voided_by": null,
"created_at": "2026-09-18T16:40:00.000Z",
"updated_at": "2026-09-18T16:40:00.000Z"
},
"sale_note": {
"id": "d4d4d4d4-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "V-118",
"display_seq": 118,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000002",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"lead_id": null,
"converted_from_reservation_id": "d3d3d3d3-0000-4000-8000-000000000001",
"seller_of_record": "automotora",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"net_clp": 11900000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "issued",
"issued_at": "2026-09-19T13:00:00.000Z",
"approved_by": null,
"approved_at": null,
"voided_at": null,
"void_reason": null,
"voided_by": null,
"created_at": "2026-09-19T13:00:00.000Z",
"updated_at": "2026-09-19T13:00:00.000Z"
},
"carried_payments": [
{
"id": "d6d6d6d6-0000-4000-8000-000000000001",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"reservation_id": null,
"sale_note_id": "d4d4d4d4-0000-4000-8000-000000000001",
"carried_from_reservation_id": "d3d3d3d3-0000-4000-8000-000000000001",
"instrument": "transferencia",
"amount_clp": 300000,
"paid_on": "2026-09-18",
"bank": "Banco Estado",
"account_number": null,
"document_number": null,
"note": "abono al tomar la reserva",
"card_fee_bps": null,
"card_surcharge_clp": null,
"card_surcharge_source": null,
"created_at": "2026-09-18T16:41:02.000Z"
}
]
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
POST /reservations/{id}/void
Void a reservation
A distinct reservations:void authority from reservations:write.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | any | yes |
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
void_reason | string | yes | máx. 2000 |
curl -X POST https://api.vitrinadev.com/api/v1/reservations/<id>/void \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"void_reason": "el cliente no completó el pago del abono a tiempo"
}'Example response (200)
{
"data": {
"id": "d3d3d3d3-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"display_id": "R-22",
"display_seq": 22,
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000001",
"holder_contact_id": "22222222-0000-4000-8000-000000000002",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"agreed_price_clp": 11900000,
"abono_expected_clp": null,
"taken_at": "2026-09-18T16:40:00.000Z",
"hold_expires_on": "2026-09-25",
"keep_advertised": true,
"status": "anulada",
"abono_disposition": null,
"abono_disposition_reason": null,
"abono_disposition_at": null,
"abono_disposition_by": null,
"voided_at": "2026-09-20T10:05:00.000Z",
"void_reason": "el cliente no completó el pago del abono a tiempo",
"voided_by": "11111111-0000-4000-8000-000000000001",
"created_at": "2026-09-18T16:40:00.000Z",
"updated_at": "2026-09-18T16:40:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429