VitrinaAPI

Purchase notes

The instrument that creates a vehicle's cost basis: issue, correct 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/purchase-notesList notas de compra
POST/purchase-notesIssue a nota de compra
GET/purchase-notes/{id}Fetch one nota de compra
PATCH/purchase-notes/{id}Correct a nota de compra
POST/purchase-notes/{id}/voidVoid a nota de compra

GET /purchase-notes

List notas de compra

The dealership’s PURCHASE documents, newest first by issued_at. A nota de compra is the instrument that CREATES a cost — the unit’s cost basis comes from it, linked through vehicle_acquisition.purchase_note_id.

Voided documents are INCLUDED by default — a voided nota de compra keeps its folio and stays listable so "where is nota de compra #47?" has an answer (ADR 0058 §10, BR-650–655). Pass exclude_voided=true to see only live ones; only the literal string true opts in, so exclude_voided=false does not hide them.

Line items are NOT included here — a list of totals should not fan out into every document’s breakdown. Use GET /purchase-notes/\{id\} for that.

Each row carries the unit it bought: vehicle is \{ id, make, model, year, registration_number \} or null, and vehicle_count says how many units the document acquired. One nota de compra may acquire SEVERAL units, so vehicle is the FIRST of them (deterministic: acquired_on, then created_at, then id) and vehicle_count is what keeps that from reading as "this note bought one car". vehicle is null exactly when vehicle_count is 0, which is an ordinary state: the document is issued before anything is attached to it, and attaching is a separate act on /vehicle-acquisitions.

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
seller_contact_idqueryuuidno
limitqueryintegerno≥ 1, ≤ 200
curl https://api.vitrinadev.com/api/v1/purchase-notes \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "d5d5d5d5-0000-4000-8000-000000000001",
      "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
      "display_id": "P-9",
      "display_seq": 9,
      "seller_contact_id": "22222222-0000-4000-8000-000000000003",
      "net_clp": 6200000,
      "tax_clp": null,
      "tax_treatment": "no_gravado",
      "status": "issued",
      "issued_at": "2026-09-12T10:15:00.000Z",
      "corrected_at": null,
      "corrected_by": null,
      "voided_at": null,
      "void_reason": null,
      "voided_by": null,
      "created_at": "2026-09-12T10:15:00.000Z",
      "updated_at": "2026-09-12T10:15:00.000Z"
    }
  ],
  "meta": {
    "total": 1
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /purchase-notes

Issue a nota de compra

Issues the document and allocates its folio (P-1, P-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, on the header AND again per line. 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.

lines are document_line rows, written in the SAME transaction as the document, so a folio is never spent on a document whose breakdown never arrived. The array order IS the document order; positions are assigned by the database and cannot be supplied. Omit lines for a document with a header amount and no breakdown, which is complete and ordinary. Whether the lines add up to the header is deliberately NOT enforced.

This does NOT attach the document to a vehicle. Recording that a unit arrived under this note is a separate act on /vehicle-acquisitions, because the ownership regime is imposed by the document and never chosen (BR-1446) — and one nota de compra may acquire several units.

seller_contact_id must belong to the calling workspace; a foreign id and a nonexistent one answer identically (404), deliberately, because a difference between them is an existence oracle. Unknown body keys are a 400tax is one character from tax_clp, and silently discarding it would record "no IVA was entered" for a caller who entered it. Answers 201.

Body

FieldTypeRequiredConstraints
seller_contact_idstring | null
net_clpintegeryes≥ 0, ≤ 999999999999
tax_clpinteger | null≥ 0, ≤ 999999999999
tax_treatmentafecto \exento \no_gravado \
issued_atstring | nulldate-time
linesobject[]
acquisitionobject
curl -X POST https://api.vitrinadev.com/api/v1/purchase-notes \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "seller_contact_id": "22222222-0000-4000-8000-000000000003",
    "net_clp": 6200000,
    "tax_treatment": "no_gravado",
    "lines": [
      {
        "description": "Compra vehículo usado — Mazda CX-5 2020",
        "amount_clp": 6200000
      }
    ]
  }'

Example response (201)

{
  "data": {
    "id": "d5d5d5d5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "display_id": "P-9",
    "display_seq": 9,
    "seller_contact_id": "22222222-0000-4000-8000-000000000003",
    "net_clp": 6200000,
    "tax_clp": null,
    "tax_treatment": "no_gravado",
    "status": "issued",
    "issued_at": "2026-09-12T10:15:00.000Z",
    "corrected_at": null,
    "corrected_by": null,
    "voided_at": null,
    "void_reason": null,
    "voided_by": null,
    "created_at": "2026-09-12T10:15:00.000Z",
    "updated_at": "2026-09-12T10:15:00.000Z"
  }
}

Answers: 201 · 400 · 401 · 403 · 404 · 409 · 429

GET /purchase-notes/{id}

Fetch one nota de compra

Accepts the UUID or the folio (P-47) — the folio is the form a dealer actually has in hand. Only the P- series resolves here; another entity’s display id (V-47, T-1247) is not a nota de compra.

Returns the document WITH its line items in document order. corrected_at / corrected_by say whether it has been changed since it was issued, and by whom.

A document belonging to another workspace is a 404, indistinguishable from one that does not exist.

ParameterInTypeRequiredConstraints
idpathanyyes
curl https://api.vitrinadev.com/api/v1/purchase-notes/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "d5d5d5d5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "display_id": "P-9",
    "display_seq": 9,
    "seller_contact_id": "22222222-0000-4000-8000-000000000003",
    "net_clp": 6200000,
    "tax_clp": null,
    "tax_treatment": "no_gravado",
    "status": "issued",
    "issued_at": "2026-09-12T10:15:00.000Z",
    "corrected_at": null,
    "corrected_by": null,
    "voided_at": null,
    "void_reason": null,
    "voided_by": null,
    "created_at": "2026-09-12T10:15:00.000Z",
    "updated_at": "2026-09-12T10:15:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

PATCH /purchase-notes/{id}

Correct a nota de compra

Corrects a LIVE document and stamps it: corrected_at and the acting principal are written onto the row, so a reader of the document can tell it has been changed since it was issued without joining an audit table. The folio and the status are never touched.

An ABSENT field is left alone; an explicit null CLEARS it. On tax_clp that is the difference between "not recorded" and "no tax", which ADR 0058 §2 keeps apart — so correcting away an amount entered by mistake is expressible without being confused with zero.

lines REPLACES the whole breakdown, in order: pass the complete list, or omit the field to leave the existing lines untouched. [] clears them, which is a legitimate correction of a breakdown entered against the wrong note. The replacement is delete-all-then-reinsert inside one transaction, because the per-document ordering key is not deferrable.

A voided document cannot be corrected — that is a 409, because voiding withdraws the instrument and correcting a withdrawn accountable record would rewrite something a third party may already have been shown. Issue a replacement instead; the voided one keeps its folio and stays listable.

A correction must state at least one field — an empty body is a 400, not a no-op stamp. PATCH and not PUT: the body is a partial and absent vs null are different instructions, which replace-the-representation semantics cannot express. Requires purchase_notes:write, the same authority as issuing.

ParameterInTypeRequiredConstraints
idpathanyyes

Body

FieldTypeRequiredConstraints
seller_contact_idstring | null
net_clpinteger≥ 0, ≤ 999999999999
tax_clpinteger | null≥ 0, ≤ 999999999999
tax_treatmentafecto \exento \no_gravado \
issued_atstringdate-time
linesobject[]
curl -X PATCH https://api.vitrinadev.com/api/v1/purchase-notes/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "net_clp": 6150000
  }'

Example response (200)

{
  "data": {
    "id": "d5d5d5d5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "display_id": "P-9",
    "display_seq": 9,
    "seller_contact_id": "22222222-0000-4000-8000-000000000003",
    "net_clp": 6150000,
    "tax_clp": null,
    "tax_treatment": "no_gravado",
    "status": "issued",
    "issued_at": "2026-09-12T10:15:00.000Z",
    "corrected_at": "2026-09-13T09:00:00.000Z",
    "corrected_by": "11111111-0000-4000-8000-000000000001",
    "voided_at": null,
    "void_reason": null,
    "voided_by": null,
    "created_at": "2026-09-12T10:15:00.000Z",
    "updated_at": "2026-09-12T10:15:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

POST /purchase-notes/{id}/void

Void a nota de compra

Voids the document. It is never deleted and it KEEPS ITS FOLIO (ADR 0058 §10, BR-650–655): the row records the time, the reason and the acting principal, keeps its line items and its correction mark, and remains listable. There is deliberately no DELETE verb on this resource.

void_reason is mandatory and must contain at least one letter or digit — a reason made only of whitespace, punctuation, symbols or invisible characters states nothing and is refused, at the edge and by a table CHECK.

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 purchase_notes:void, which is a DIFFERENT authority from purchase_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/purchase-notes/<id>/void \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "void_reason": "se anuló la compra: el vendedor desistió antes de firmar"
  }'

Example response (200)

{
  "data": {
    "id": "d5d5d5d5-0000-4000-8000-000000000001",
    "tenant_id": "a1a1a1a1-0000-4000-8000-000000000001",
    "display_id": "P-9",
    "display_seq": 9,
    "seller_contact_id": "22222222-0000-4000-8000-000000000003",
    "net_clp": 6200000,
    "tax_clp": null,
    "tax_treatment": "no_gravado",
    "status": "voided",
    "issued_at": "2026-09-12T10:15:00.000Z",
    "corrected_at": null,
    "corrected_by": null,
    "voided_at": "2026-09-14T11:30:00.000Z",
    "void_reason": "se anuló la compra: el vendedor desistió antes de firmar",
    "voided_by": "11111111-0000-4000-8000-000000000001",
    "created_at": "2026-09-12T10:15:00.000Z",
    "updated_at": "2026-09-12T10:15:00.000Z"
  }
}

Answers: 200 · 400 · 401 · 403 · 404 · 409 · 429

On this page