Notas de venta
El documento que vende el auto: emitir, aprobar (lo que lo hace legalmente válido y marca la unidad vendida) y anular.
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 | /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) |
PATCH | /sale-notes/{id}/buyer | Assign or change the buyer of a nota de venta |
GET | /sale-notes/{id}/participations | The credited participations on a sale, with totals |
PATCH | /sale-notes/{id}/participations/{participationId} | Record the missing commission on a participation |
GET | /sale-notes/{id}/pdf | Print the nota de venta |
POST | /sale-notes/{id}/void | Void a nota de venta |
GET | /sale-notes/export | Export the notas de venta (xlsx / csv) |
GET | /sale-notes/vendedor-default | The Vendedor a new nota de venta would get |
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.
economics — only for callers holding dealership_economics:read (absent, not null, for everyone else): margen_bruto_clp (Margen bruto), margen_neto_clp (Utilidad del negocio), comisiones_vendedores_clp (Comisión) and canal (where the buyer came from), each the SAME figure GET /margin serves — one definition, computed in batch for the page. A figure that is not computable is null, never zero, and gaps names the missing input (cost_rollup = Sin costo, seller_commission = Sin comisión, liquidacion = Sin liquidación). A voided row has economics: null.
status narrows to one derived status; status=issued is the «Por aprobar» list (emitted, waiting for its approval — ADR 0114 §4).
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
exclude_voided | query | boolean | null | no | |
vehicle_id | query | uuid | no | |
lead_id | query | any | no | |
status | query | issued \ | approved \ | voided |
limit | query | integer | no | ≥ 1, ≤ 200 |
curl https://api.vitrinadev.com/api/v1/sale-notes \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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,
"list_price_clp": 12490000,
"discount_clp": 590000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "issued",
"issued_at": "2026-09-19T13:00:00.000Z",
"emitted_by": "11111111-0000-4000-8000-000000000001",
"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
}
}Responde: 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.
emitted_by is the caller (ADR 0114 §4): the member (also behind a personal token) or the API key that issued it, stamped from the credential — there is no body field for it.
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.
Every sale has exactly one Vendedor (ADR 0114 §3) — the team member who closed it: the closer participation, echoed on salesperson_id. When the request names nobody, the Vendedor defaults to the assignee of the lead_id deal, else the car's Responsable del auto (vehicle.responsible_user_id), skipping anyone who is no longer an active member. When neither resolves, the emit is a 400 with field_errors.salesperson_id and no folio consumed. The default is a proposal: naming someone else always wins.
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. Its payments are validated BEFORE anything is written (a malformed row is a 400 with no folio consumed) and 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-recorded sale. price + Σ statutory add-ons is weighed against Σ these payments + retoma + credit: a plan that comes up short still issues (201) — the usual case of a sale issued with only the deposit — and the note reads funding.standing: "short" with funding.gap_clp on GET /sale-notes/\{id\}; record the rest later with POST /document-payments. 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. A plan stated against a document with no tax_clp is a 400 (the balance cannot be computed: an unrecorded IVA is not zero).
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.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
vehicle_id | uuid | sí | |
buyer_contact_id | string | null | — | |
lead_id | any | — | |
seller_of_record | consignante \ | retoma \ | automotora |
salesperson_id | string | null | — | |
participations | object[] | — | |
net_clp | integer | sí | ≥ 0, ≤ 999999999999 |
tax_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
tax_treatment | afecto \ | exento \ | no_gravado \ |
expected_list_price_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
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"
}'Ejemplo de respuesta (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,
"list_price_clp": 12490000,
"discount_clp": 590000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "issued",
"issued_at": "2026-09-19T13:00:00.000Z",
"emitted_by": "11111111-0000-4000-8000-000000000001",
"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"
}
}Responde: 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.
timeline says who did what, by name (ADR 0114 §4): one entry per step that happened, in the order the document lives them — emitted (issued_at, emitted_by), approved, printed (the compraventa in force) and voided — each with its instant and its actor (\{ id, name, kind \}, kind = member or api_key). actor is null when the document does not record who did it (a note emitted before emitted_by existed); actor.name is null when that person has no name on file. Show either in words — never as an anonymous teammate. Approval is a separate step under sale_notes:write, so the emitter and the approver are often different people.
receivable_obligation_id is the open ledger receivable this note settles — pass it as obligation_id to POST /payments to apply a payment to this note. null once the note is voided (the void releases its money) or when it has no receivable.
print_missing_fields is what GET /sale-notes/\{id\}/pdf would refuse with right now — the SAME \{ variable, label, fix, contact_id? \} list as its 409 details.missing_fields — so a screen can ask for the buyer’s domicilio while the buyer is still at the counter. [] when the note prints, and always [] on a voided note.
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
curl https://api.vitrinadev.com/api/v1/sale-notes/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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,
"list_price_clp": 12490000,
"discount_clp": 590000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "issued",
"issued_at": "2026-09-19T13:00:00.000Z",
"emitted_by": "11111111-0000-4000-8000-000000000001",
"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",
"receivable_obligation_id": "01a1c1df-a6ea-779f-9310-bdbb2eadd184",
"print_missing_fields": [
{
"variable": "COMPRADOR_DOMICILIO",
"label": "Domicilio del comprador",
"fix": "buyer_contact",
"contact_id": "01a1ad2b-a52a-7b04-abf8-389e20e8217a"
}
],
"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,
"timeline": [
{
"step": "emitted",
"at": "2026-09-19T13:00:00.000Z",
"actor": {
"id": "11111111-0000-4000-8000-000000000001",
"name": "Martina Reyes",
"kind": "member"
}
}
]
}
}Responde: 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 salesperson cannot authorise their own discount). Raise one withPOST /price-approvalsand have somebody else decide it. A caller holdingprice_approval:approve(an owner or admin) is not refused: approving the sale is their authorisation of the price. 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 car is consigned (active contrato de consignación) and its mandato was never emitted — «Emite el mandato antes de aprobar» (
details.reason: mandate_not_emitted). Emit it withPOST /consignments/\{id\}/contract. - 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).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|
curl -X POST https://api.vitrinadev.com/api/v1/sale-notes/<id>/approve \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{}'Ejemplo de respuesta (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,
"list_price_clp": 12490000,
"discount_clp": 590000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "approved",
"issued_at": "2026-09-19T13:00:00.000Z",
"emitted_by": "11111111-0000-4000-8000-000000000001",
"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"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /sale-notes/{id}/buyer
Assign or change the buyer of a nota de venta
Sets buyer_contact_id on a live (issued or approved) nota de venta — the fix for a note issued without its comprador, without voiding it. The contact must belong to this workspace (404 otherwise, or when it was merged away). Responds with the same read as GET /sale-notes/\{id\}. Setting the buyer it already has is a no-op 200.
409 with details.blocked_reason and a Spanish message when the buyer is already written into something else: sale_note_voided, compraventa_issued (the contract names the buyer), trade_ins_recorded (a retoma came from the current buyer) or credit_attached (the credit’s borrower is the current buyer). The way out for those is void + re-issue. Requires sale_notes:write.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
buyer_contact_id | uuid | sí |
curl -X PATCH https://api.vitrinadev.com/api/v1/sale-notes/<id>/buyer \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"buyer_contact_id": "22222222-0000-4000-8000-000000000002"
}'Ejemplo de respuesta (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,
"list_price_clp": 12490000,
"discount_clp": 590000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "issued",
"issued_at": "2026-09-19T13:00:00.000Z",
"emitted_by": "11111111-0000-4000-8000-000000000001",
"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",
"receivable_obligation_id": "01a1c1df-a6ea-779f-9310-bdbb2eadd184",
"print_missing_fields": [
{
"variable": "COMPRADOR_DOMICILIO",
"label": "Domicilio del comprador",
"fix": "buyer_contact",
"contact_id": "01a1ad2b-a52a-7b04-abf8-389e20e8217a"
}
],
"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,
"timeline": [
{
"step": "emitted",
"at": "2026-09-19T13:00:00.000Z",
"actor": {
"id": "11111111-0000-4000-8000-000000000001",
"name": "Martina Reyes",
"kind": "member"
}
}
]
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /sale-notes/{id}/participations
The credited participations on a sale, with totals
Who is credited on a nota de venta (by UUID or folio V-47) and what each earns: its Vendedor (role: closer, exactly one) and any Colaborador (role: collaborator). Each participation carries its id — the participationId that PATCH /sale-notes/\{id\}/participations/\{participationId\} takes — and its frozen commission (commission_rate_bp × commission_basis_clp = commission_clp). A participation whose three commission fields are null makes the sale «Sin comisión»: totals.complete is false, totals.gaps names who, and the sale’s Utilidad del negocio is not computable until it is recorded. Needs seller_compensation:read.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí |
curl https://api.vitrinadev.com/api/v1/sale-notes/<id>/participations \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"sale_note_id": "d4d4d4d4-0000-4000-8000-000000000001",
"display_id": "V-23",
"participations": [
{
"id": "5d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"sale_note_id": "d4d4d4d4-0000-4000-8000-000000000001",
"user_id": "11111111-0000-4000-8000-000000000001",
"role": "closer",
"commission_rate_bp": 250,
"commission_basis_clp": 1800000,
"commission_clp": 45000,
"created_at": "2026-09-14T11:30:00.000Z",
"updated_at": "2026-09-14T11:30:00.000Z"
},
{
"id": "6e2d3c4b-5f60-4b7c-9d8e-0f1a2b3c4d5e",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"sale_note_id": "d4d4d4d4-0000-4000-8000-000000000001",
"user_id": "11111111-0000-4000-8000-000000000002",
"role": "collaborator",
"commission_rate_bp": null,
"commission_basis_clp": null,
"commission_clp": null,
"created_at": "2026-09-14T11:30:00.000Z",
"updated_at": "2026-09-14T11:30:00.000Z"
}
],
"totals": {
"closer_commission_clp": 45000,
"collaborator_commission_clp": 0,
"total_commission_clp": 45000,
"complete": false,
"gaps": [
"11111111-0000-4000-8000-000000000002"
]
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /sale-notes/{id}/participations/{participationId}
Record the missing commission on a participation
Fixes a «Sin comisión» sale: a participation written with no commission — its Vendedor had no compensation scheme when the nota de venta was emitted, or the car had no recorded cost (no computable Margen bruto to apply a rate to) — gets one. commission_rate_bp defaults to the member’s scheme as of the SALE’s day; commission_basis_clp defaults to the sale’s Margen bruto as it stands now (so: record the cost, then the commission). 409 when the participation already has a commission — a recorded commission is frozen and never restated — when the Margen bruto is still not computable and no basis is stated, or on a voided nota de venta. 400 when no rate is stated and the member had no scheme on that day. A fixed amount agreed for this sale is commission_clp instead: it is recorded as 100 % of that amount (commission_rate_bp 10000, commission_basis_clp = the amount) and cannot be combined with the other two (400). Once every participation has its commission, the sale’s Utilidad del negocio is computable and it enters Margen y rentabilidad’s totals.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí | |
participationId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
commission_rate_bp | integer | — | ≥ 0, ≤ 10000 |
commission_basis_clp | integer | — | ≥ 0, ≤ 999999999999 |
commission_clp | integer | — | ≥ 0, ≤ 999999999999 |
curl -X PATCH https://api.vitrinadev.com/api/v1/sale-notes/<id>/participations/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"commission_rate_bp": 250
}'Ejemplo de respuesta (200)
{
"data": {
"id": "5d1c2b3a-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
"sale_note_id": "d4d4d4d4-0000-4000-8000-000000000001",
"user_id": "11111111-0000-4000-8000-000000000001",
"role": "closer",
"commission_rate_bp": 250,
"commission_basis_clp": 1800000,
"commission_clp": 45000,
"created_at": "2026-09-14T11:30:00.000Z",
"updated_at": "2026-09-29T10:05:00.000Z"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /sale-notes/{id}/pdf
Print the nota de venta
The nota de venta as a printable PDF (application/pdf, served inline): the workspace’s default sale_note template — or template_id — filled with this sale’s facts (dealership legal identity, folio, ejecutivo, buyer, seller, vehicle, price, payments). With no live template it prints the standard text. Rendered on demand and not stored (the frozen legal instrument is the compraventa). Any status prints; a voided note carries an «ANULADA» banner. The X-Template-Version header names the prose used.
409 when a fact the template needs is missing — nothing is printed with a blank. details.blocked_reason is sale_note_print_incomplete (or the legal-seller code when the seller is what is missing), details.missing lists Spanish labels (e.g. «RUT de la automotora», «RUT del comprador») and details.missing_fields[] carries \{ variable, label, fix, contact_id? \} where fix is organization_legal | buyer_contact | owner_contact | sale_note | legal_seller — the form to open. Requires sale_notes:read.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | any | sí | |
template_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/sale-notes/<id>/pdf \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 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.
| 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/sale-notes/<id>/void \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"void_reason": "el banco rechazó el financiamiento del comprador"
}'Ejemplo de respuesta (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,
"list_price_clp": 12490000,
"discount_clp": 590000,
"tax_clp": 1900000,
"tax_treatment": "afecto",
"status": "voided",
"issued_at": "2026-09-19T13:00:00.000Z",
"emitted_by": "11111111-0000-4000-8000-000000000001",
"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"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /sale-notes/export
Export the notas de venta (xlsx / csv)
The Ventas list as a planilla: one row per nota de venta issued in the window — folio, issue and approval dates, status, patente, vehicle, regime, seller, buyer, the buyer’s origin channel, the published price, the discount (amount and %), the sale price, IVA, tax treatment and the void reason. No cost figure (that is GET /margin/export). from/to are inclusive YYYY-MM-DD days on the dealership’s calendar, status narrows to issued | approved | voided; all optional. format is xlsx (default) or csv. 413 over 2000 documents. Requires sale_notes:read.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
from | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
to | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
status | query | issued \ | approved \ | voided |
format | query | xlsx \ | csv | no |
curl https://api.vitrinadev.com/api/v1/sale-notes/export \
-H "Authorization: Bearer $VITRINA_KEY"Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /sale-notes/vendedor-default
The Vendedor a new nota de venta would get
Who POST /sale-notes credits as the Vendedor when its body names nobody (ADR 0114 §3), so a screen or an agent can propose that person up front: the assignee of the sale’s lead — the lead_id given, else the buyer’s lead on this car (buyer_contact_id + vehicle_id) — otherwise the car’s Responsable del auto. A member who left the workspace is skipped. source says which rung answered (lead_owner | vehicle_responsible) and lead (id, display_id) names the lead when it did. data is null when nobody can be proposed: the emit is then refused until a Vendedor is chosen. The inferred lead only proposes the Vendedor; it is never written as the nota de venta’s lead_id.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
vehicle_id | query | uuid | sí | |
buyer_contact_id | query | uuid | no | |
lead_id | query | any | no |
curl https://api.vitrinadev.com/api/v1/sale-notes/vendedor-default \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"user_id": "11111111-0000-4000-8000-000000000002",
"source": "lead_owner",
"lead": {
"id": "99999999-0000-4000-8000-000000000001",
"display_id": "L-47"
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429