VitrinaAPI

Sale notes

The document that sells the car: issue, approve (the instant it becomes legally valid and the unit is marked sold) and void.

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.

Sell a unit explains this resource in prose, with runnable examples.

MethodPathWhat it does
GET/sale-notesList notas de venta
POST/sale-notesIssue a nota de venta
GET/sale-notes/{id}Fetch one nota de venta
POST/sale-notes/{id}/approveApprove a nota de venta (the sale fan-out)
POST/sale-notes/{id}/voidVoid 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.

ParameterInTypeRequiredConstraints
exclude_voidedqueryboolean | nullno
vehicle_idqueryuuidno
lead_idqueryanyno
limitqueryintegerno≥ 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

FieldTypeRequiredConstraints
vehicle_iduuidyes
buyer_contact_idstring | null—
lead_idany—
seller_of_recordconsignante \retoma \automotora
salesperson_idstring | null—
participationsobject[]—
net_clpintegeryes≥ 0, ≤ 999999999999
tax_clpinteger | null—≥ 0, ≤ 999999999999
tax_treatmentafecto \exento \no_gravado \
issued_atstring | null—date-time
approved_bystring | null—
approved_atstring | null—date-time
fundingobject | null—
trade_insobject[]—
creditobject | null—
carry_reservation_abonoboolean—
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.

ParameterInTypeRequiredConstraints
idpathanyyes
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_approval names this document (BR-433/BR-434 — a sale cannot authorise its own discount). Raise one with POST /price-approvals and have somebody else decide it. The refusal deliberately states no figure: the shortfall and the reference are cost disclosures gated on dealership_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).

ParameterInTypeRequiredConstraints
idpathanyyes

Body

FieldTypeRequiredConstraints
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.

ParameterInTypeRequiredConstraints
idpathanyyes

Body

FieldTypeRequiredConstraints
void_reasonstringyesmá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

On this page