Sale notes
The document that sells the car: issue, approve (the instant it becomes legally valid and the unit is marked sold) and void.
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 | /sale-notes | List notas de venta |
POST | /sale-notes | Issue a nota de venta |
GET | /sale-notes/{id} | Fetch one nota de venta |
POST | /sale-notes/{id}/approve | Approve a nota de venta (the sale fan-out) |
POST | /sale-notes/{id}/void | Void a nota de venta |
GET /sale-notes
List notas de venta
The dealership’s sale documents, newest first by issued_at. Voided documents are INCLUDED by default — a voided nota de venta keeps its folio and stays listable so "where is nota de venta #47?" has an answer (ADR 0058 §10). Pass exclude_voided=true to see only live ones; only the literal string true opts in, so exclude_voided=false does not hide them.
lead_id answers "which nota de venta closed this deal?" (ERP S15-16) — a lead UUID or its folio (L-89); a foreign or unknown lead is a 404. It does NOT imply exclude_voided, deliberately: a voided document must stay reachable from the deal it closed, or the void becomes the one event that erases the answer. So a deal that was closed, voided and re-issued returns both documents here, and the live one is the row whose status is issued.
Every row carries totals — the SAME object GET /sale-notes/\{id\} embeds, computed by the same roll-up: total_a_pagar_clp (price + IVA + Σ statutory add-ons), total_pagos_clp, saldo_clp, payment_count, complete and gaps. ⚠ A POSITIVE saldo_clp means the customer has OVERPAID; a NEGATIVE one is what they still owe (BR-245 — it is the sign the printed document uses). total_a_pagar_clp and saldo_clp are both null when tax_clp was never recorded, with gaps naming it: that is "not computable", never zero, and a client must render it as such rather than treating the unrecorded IVA as 0. Voided rows are totalled like any other — the money against them was really taken, and status is what says the document is dead. Do not sum the payments client-side; there is one definition of this balance and this is it.
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 |
|---|---|---|---|---|
exclude_voided | query | boolean | null | no | |
vehicle_id | query | uuid | no | |
lead_id | query | any | no | |
limit | query | integer | no | ≥ 1, ≤ 200 |
curl https://api.vitrinadev.com/api/v1/sale-notes \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": [
{
"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": null,
"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"
}
],
"meta": {
"total": 1
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /sale-notes
Issue a nota de venta
Issues the document and allocates its folio (V-1, V-2, …) inside the inserting transaction, so a failed issue RELEASES the number rather than burning it out of a legally accountable series.
Money is whole CLP pesos (bigint, never minor units, never decimals) and the triple is net_clp + tax_clp + tax_treatment. Omit tax_clp to record "not recorded"; pass 0 to record "no tax" — ADR 0058 §2 makes those two different claims and nothing collapses them. tax_treatment is REQUIRED and has no default: the same peso falls inside or outside the IVA base depending on how the deal is structured, so a default would be a guess stored as a determination.
vehicle_id, buyer_contact_id and lead_id must belong to the calling workspace — no foreign key carries a tenant, so this is checked before the insert. A foreign id and a nonexistent one answer identically (404), deliberately: a difference between them is an existence oracle. A soft-deleted or merged vehicle is also refused.
lead_id names the deal this document CLOSES (ERP S15-16) — a lead UUID or its folio (L-89). Omit it for a walk-in sale with no CRM deal; absent means "no originating deal", never "unknown". The link is then queryable both ways: GET /sale-notes?lead_id=… from the deal, and the lead_id field on the document itself. A deal has at most one live closing document — a second one is a 409 until the first is voided, enforced by a partial unique index rather than by a pre-read, so two concurrent issues cannot both win. The link is set here and never edited: there is no update endpoint, and the correction path is the one ADR 0058 §10 already defines — void (the row, its folio and its link all survive) and issue a replacement.
approved_by and approved_at are all-or-nothing. Timestamps accept either UTC (…Z) or an explicit offset (…-04:00, Chile). Unknown body keys are a 400 — tax is one character from tax_clp, and silently discarding it would record "no IVA was entered" for a caller who entered it. Answers 201.
funding IS THE BALANCE GATE (ERP S9-5, BR-418) — where the money for this sale comes from. Supplying it makes the gate BINDING on this request: price + Σ statutory add-ons is weighed against Σ these payments + retoma + credit BEFORE anything is written, a short plan is a 400 that states the gap with no folio consumed, and a plan that holds is written into the SAME transaction as the document — so a payment the database refuses rolls V-n back rather than burning it on a half-funded sale. Over-funding is allowed and comes back labelled as saldo_a_favor_del_cliente_clp; it is never an error and is never silently clamped.
There is deliberately no retoma_clp and no credit_clp inside funding: a hand-typed figure for either would be an unbacked claim the gate then counts as money. Omitting funding is not a bypass — the document issues as it always has and reads short on GET /sale-notes/\{id\} with the gap stated. Use POST /sale-notes/funding/preview to see the gap before committing to the issue.
trade_ins IS LA RETOMA (ERP S9-6, BR-451–460) — the cars the buyer is handing over in part-exchange. ⚠ A trade-in is a PAYMENT and never a price reduction (BR-455): nothing here touches net_clp or any price field, and Σ precio_toma_clp counts on the FUNDING side of the balance gate beside the payments. "If a trade-in reduced the price, any seller could inflate the take-in to disguise a discount and reported revenue would collapse."
Each car must ALREADY EXIST as a vehicle in this workspace (BR-451 — the car is about to enter stock and needs identity before it can carry a cost), must not be the car being sold (BR-452), and must not already have an acquisition record — a unit the dealership once acquired cannot come back as a trade-in yet, and that is a 409 with the rule in it. precio_toma_clp is the NEGOTIATED credit, whole CLP pesos and STRICTLY positive (BR-453): a business decision, not the car’s list price and not its SII appraisal.
Issuing with trade_ins also records HOW THE INCOMING UNIT ARRIVED (BR-458): one vehicle_acquisition of kind retoma per car, naming this document, whose cost basis is the take-in value and whose tenencia the database derives as propio — written in the SAME transaction that allocated the folio, so every refusal leaves nothing behind and consumes no V-n. The document must name its buyer_contact_id: the person trading the car in is the party the dealership bought it from, and BR-460 makes that chain the only thing that identifies the legal seller when the car is sold on.
credit IS THE BROKERED FINANCING (ERP S9-7, BR-461–464) — the financiera and the amount it is lending. ⚠ It counts in the balance gate BEFORE the cash lands (BR-463: "the sale is funded even though the cash is not in"), which is what makes a FINANCED deal balance at the counter. ⚠ It is not a payment and has no instrument: credito is deliberately absent from the customer_payment instrument vocabulary (BR-264/BR-450/BR-1010), so it is BR-418’s own term and never a payment row. institution is the credit’s identity on the document (BR-464) and is required; application_number is internal. There is no vehicle and no client field — BR-461’s pair is this note’s, already fixed.
Omitting credit is the ordinary case even for a financed deal: the dealership records financing as part of closing, after the fact (112 credits taken against 0 sent in the recovered system), through PUT /sale-notes/\{id\}/credit. The note reads short in between, with the gap stated.
Body
| Field | Type | Required | Constraints |
|---|---|---|---|
vehicle_id | uuid | yes | |
buyer_contact_id | string | null | — | |
lead_id | any | — | |
seller_of_record | consignante \ | retoma \ | automotora |
salesperson_id | string | null | — | |
participations | object[] | — | |
net_clp | integer | yes | ≥ 0, ≤ 999999999999 |
tax_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
tax_treatment | afecto \ | exento \ | no_gravado \ |
issued_at | string | null | — | date-time |
approved_by | string | null | — | |
approved_at | string | null | — | date-time |
funding | object | null | — | |
trade_ins | object[] | — | |
credit | object | null | — | |
carry_reservation_abono | boolean | — |
curl -X POST https://api.vitrinadev.com/api/v1/sale-notes \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"vehicle_id": "e1e1e1e1-0000-4000-8000-000000000002",
"buyer_contact_id": "22222222-0000-4000-8000-000000000002",
"seller_of_record": "automotora",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"net_clp": 11900000,
"tax_clp": 1900000,
"tax_treatment": "afecto"
}'Example response (201)
{
"data": {
"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": null,
"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"
}
}Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /sale-notes/{id}
Fetch one nota de venta
Accepts the UUID or the folio (V-47) — the folio is the form a dealer actually has in hand. Only the V- series resolves here; another entity’s display id (T-1247) is not a nota de venta.
A document belonging to another workspace is a 404, indistinguishable from one that does not exist.
It answers the money too. payments is every amount received against this note with the instrument it arrived as; totals carries total_a_pagar_clp / total_pagos_clp / saldo_clp (a POSITIVE saldo is an over-payment, a NEGATIVE one is what the customer still owes); and since ERP S9-5 funding carries BR-418’s verdict — standing is funded, overfunded (with saldo_a_favor_del_cliente_clp), short (with gap_clp) or not_computable, plus the five terms it was reached from. total_a_pagar_clp INCLUDES the statutory add-ons charged beside the price (BR-243).
Nothing here is stored: the standing is DERIVED from the rows on every read, there is no funding_standing column, and there is nothing to write. A document issued without a funding plan reads short, which is the honest state and not an error.
trade_ins names the cars taken in part-exchange (ERP S9-6) — one entry per car with the line’s own id (never the vehicle’s: BR-457), the vehicle_id, the precio_toma_clp allowed for it, and the acquisition_id of the vehicle_acquisition that arrival created. That is the SALE → UNIT direction; the UNIT → SALE direction needs no surface here, because vehicle_acquisition.sale_note_id is projected on every read of GET /vehicle-acquisitions?vehicle_id=. acquisition_id is null once the document has been VOIDED — a void retracts the arrival and keeps the line, so the null says exactly what happened: this credit was agreed, and the arrival it caused has been withdrawn.
credit is the LIVE brokered credit, or null (ERP S9-7) — the institution that is lending (BR-464 makes the financiera the credit’s identity on the document), the amount_clp, and its OWN state: committed (the bank has promised) or disbursed (the money has arrived, with disbursed_at / disbursed_by beside it). ⚠ Both count the same in funding (BR-463), so never read funded as "the cash is in" — read credit.state for that. The committed-vs-collected gap is exactly this pair of fields read side by side. ⚠ The credit’s state is INDEPENDENT of the document’s status: a voided nota de venta keeps its credit row untouched, and a disbursed credit does not make the document anything. Credits that were REPLACED or REMOVED are tombstoned and are deliberately not served here — they are history, not funding, and listing them beside the live one would invite a client to add them up.
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | any | yes |
curl https://api.vitrinadev.com/api/v1/sale-notes/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Example response (200)
{
"data": {
"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": null,
"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",
"payments": [
{
"id": "d6d6d6d6-0000-4000-8000-000000000002",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"reservation_id": null,
"sale_note_id": "d4d4d4d4-0000-4000-8000-000000000001",
"carried_from_reservation_id": null,
"instrument": "transferencia",
"amount_clp": 13800000,
"paid_on": "2026-09-19",
"bank": "Banco de Chile",
"account_number": null,
"document_number": null,
"note": null,
"card_fee_bps": null,
"card_surcharge_clp": null,
"card_surcharge_source": null,
"created_at": "2026-09-19T13:05:00.000Z"
}
],
"totals": {
"total_a_pagar_clp": 13800000,
"total_pagos_clp": 13800000,
"saldo_clp": 0,
"recargo_tarjeta_clp": 0,
"total_a_cobrar_clp": 13800000,
"payment_count": 1,
"complete": true,
"gaps": []
},
"funding": {
"standing": "funded",
"balanced": true,
"terms": {
"price_clp": 13800000,
"add_on_charges_clp": 0,
"payments_clp": 13800000,
"retoma_clp": 0,
"credit_clp": 0
},
"total_a_pagar_clp": 13800000,
"total_consideration_clp": 13800000,
"saldo_clp": 0,
"gap_clp": null,
"saldo_a_favor_del_cliente_clp": null,
"payment_count": 1,
"complete": true,
"gaps": []
},
"trade_ins": [],
"credit": null
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /sale-notes/{id}/approve
Approve a nota de venta (the sale fan-out)
Approves the document — the instant it becomes legally valid (BR-485) and the instant every consequence of the sale fires. All of it happens in ONE database transaction, so either all of it happened or none of it did:
- the document is stamped with WHO approved it and WHEN (BR-484);
- the car is marked
vendido(BR-486), which is TERMINAL — a sold unit never sells or lists again (ADR 0030, regla de Estadía); - every vigente cotización on that unit is voided in the same write (BR-215), so the car is never left sold with a live price commitment standing on it;
- the Negocio the note names (
lead_id) is closed WON (BR-487), by a real stage move rather than a status patch.
Afterwards the document appears in GET /sale-notes/post-sale-obligations (BR-489) and its Contrato de Compraventa and Declaración de Responsabilidad become printable (BR-488). The unit’s portal adverts are retracted immediately AFTER the transaction commits — never before, because a queue job cannot be rolled back and a rolled-back approval must not unpublish a car that was not sold.
The body is empty and strict. The approver is the authenticated principal and the instant is server-stamped; a body naming either is a 400. Back-dating an approval is expressible only on POST /sale-notes (approved_at), and only for the ADR 0058 §11 talonario cutover.
Refusals write NOTHING — not the stamp, not the sold flag, not the quote deaths, not the deal close, and no advert job:
- 409 when the sale’s price is below the level this organisation requires an authorisation for and no APPROVED
price_approvalnames this document (BR-433/BR-434 — a sale cannot authorise its own discount). Raise one withPOST /price-approvalsand have somebody else decide it. The refusal deliberately states no figure: the shortfall and the reference are cost disclosures gated ondealership_economics:read. - 409 when the unit has already been sold by another document.
- 409 when the document is already approved, or is voided — an approval names a person and a moment and cannot be restated.
- 400 when the deal’s board has no Won column, or forbids the move onto it.
Two simultaneous approvals resolve to exactly one 200 and one 409, and the fan-out runs once.
Requires sale_notes:write — approving is the completion of the same act as issuing, not a separate authority (unlike sale_notes:void).
| Parameter | In | Type | Required | Constraints |
|---|---|---|---|---|
id | path | any | yes |
Body
| Field | Type | Required | Constraints |
|---|
curl -X POST https://api.vitrinadev.com/api/v1/sale-notes/<id>/approve \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{}'Example response (200)
{
"data": {
"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": null,
"seller_of_record": "automotora",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"net_clp": 11900000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "approved",
"issued_at": "2026-09-19T13:00:00.000Z",
"approved_by": "11111111-0000-4000-8000-000000000001",
"approved_at": "2026-09-19T14:10:00.000Z",
"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"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /sale-notes/{id}/void
Void a nota de venta
Voids the document. 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 listable. There is deliberately no DELETE verb on this resource.
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 a reason nobody can see is not a reason.
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. Voiding twice is a 409: the first actor and reason stand.
Requires sale_notes:void, which is a DIFFERENT authority from sale_notes:write — holding the write scope does not permit a void.
| 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/sale-notes/<id>/void \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"void_reason": "el banco rechazó el financiamiento del comprador"
}'Example response (200)
{
"data": {
"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": null,
"seller_of_record": "automotora",
"salesperson_id": "11111111-0000-4000-8000-000000000001",
"net_clp": 11900000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "voided",
"issued_at": "2026-09-19T13:00:00.000Z",
"approved_by": null,
"approved_at": null,
"voided_at": "2026-09-20T09:00:00.000Z",
"void_reason": "el banco rechazó el financiamiento del comprador",
"voided_by": "11111111-0000-4000-8000-000000000001",
"created_at": "2026-09-19T13:00:00.000Z",
"updated_at": "2026-09-19T13:00:00.000Z"
}
}Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429