VitrinaAPI

Charges and payments

The workspace ledger: what is owed, what arrived and how the second is allocated to the first.

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.

Charge and reconcile payments explains this resource in prose, with runnable examples.

MethodPathWhat it does
GET/contacts/{id}/ledgerEverything one person owes, has paid, and has in their favour
GET/obligationsWhat is owed, by state, contact, kind or group
POST/obligationsRecord what somebody owes
GET/obligations/{id}One cobro, its derived state and its allocation trail
PATCH/obligations/{id}Adjust what is owed, its deadline or its label — with a reason
POST/obligations/{id}/cancelCall off a cobro, and say what became of the money
POST/obligations/{id}/waiveForgive what is owed, with a reason
GET/obligations/groups/{groupId}A presupuesto, pack or plan and every line in it
GET/paymentsMoney that arrived — the Transferencias queue, or one contact
POST/paymentsRecord money by hand: cash, a card, a bono, a cheque
GET/payments/{id}One payment with its full allocation trail
POST/payments/{id}/allocationsAllocate — or re-allocate — a payment across obligations
DELETE/payments/{id}/allocations/{allocationId}Release one allocation, with a reason
POST/payments/{id}/assign-payerSay whose money a payment is (or take the name off it)
POST/payments/{id}/reverseReverse a payment — the trio, and every allocation released

GET /contacts/{id}/ledger

Everything one person owes, has paid, and has in their favour

payments:read — NOT contacts:read: what a person owes and has paid is strictly more than the ficha's identity surface.

THE ONE SHARED READ (§6.1): the ficha's Pagos tab, the agenda drawer and the agent's bounded read tool all consume this shape. credit_clp is the unallocated surplus (Σ live payments − Σ live allocations) and is NEVER a column; outstanding_clp sums the open obligations. It reads the WHOLE set and does not page, deliberately — a paged read would compute a credit that is too small, and do it silently.

pending_verification is EVIDENCE, NOT MONEY: payments whose payer is only a SUGGESTION for this contact. The platform has seen something arrive and has not attributed it, and stating it as received would be the platform confirming a transfer nobody has matched.

EVERY payments[] ROW CARRIES ITS CARD RECARGO — card_surcharge_clp (what the customer paid ON TOP, so the screen can draw «Recargo tarjeta $X»), card_fee_bps (the rate it was frozen at) and card_surcharge_source (calculado = that rate applied, ajustado = a figure an operator typed). ⚠ IT IS NOT PART OF amount_clp AND NOT PART OF ANY TOTAL HERE: credit_clp, allocated_clp and outstanding_clp are all net of it, because the recargo covers the card machine and settles nothing. A screen that wants what the terminal charged adds the two itself; a screen that wants what was paid towards the cobro uses amount_clp alone.

ParameterInTypeRequiredConstraints
idpathuuidyes
todayquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
curl https://api.vitrinadev.com/api/v1/contacts/<id>/ledger \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "credit_clp": 0,
    "outstanding_clp": 296000,
    "obligations": [
      {
        "id": "b44936d0-0000-4000-8000-000000000004",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "kind": "service",
        "direction": "receivable",
        "label": "Ortodoncia — primera cuota",
        "expected_clp": 108000,
        "allocated_clp": 50000,
        "outstanding_clp": 58000,
        "overpaid_clp": 0,
        "state": "partial",
        "provenance": "manual",
        "is_overdue": false,
        "status": "open",
        "disposition": null,
        "opened_on": "2026-09-22",
        "due_on": "2026-10-10",
        "closed_at": null,
        "closed_reason": null,
        "group_id": null,
        "source": "staff",
        "document": null,
        "allocations": [
          {
            "id": "e8df2087-0000-4000-8000-000000000001",
            "customer_payment_id": "e86256b2-0000-4000-8000-000000000001",
            "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
            "amount_clp": 50000,
            "method": "staff",
            "reasons": [
              "allocation_staff_manual"
            ],
            "allocated_by": null,
            "allocated_at": "2026-09-22T22:21:16.990Z",
            "released_at": null,
            "release_reason": null,
            "live": true
          }
        ]
      },
      {
        "id": "b44936d0-0000-4000-8000-000000000003",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "kind": "service",
        "direction": "receivable",
        "label": "Limpieza dental",
        "expected_clp": 30000,
        "allocated_clp": 0,
        "outstanding_clp": 30000,
        "overpaid_clp": 0,
        "state": "pending",
        "provenance": null,
        "is_overdue": false,
        "status": "open",
        "disposition": null,
        "opened_on": "2026-09-22",
        "due_on": null,
        "closed_at": null,
        "closed_reason": null,
        "group_id": "a067dce6-0000-4000-8000-000000000002",
        "source": "staff",
        "document": null,
        "allocations": []
      },
      {
        "id": "b44936d0-0000-4000-8000-000000000002",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "kind": "service",
        "direction": "receivable",
        "label": "Control de ortodoncia",
        "expected_clp": 112000,
        "allocated_clp": 0,
        "outstanding_clp": 112000,
        "overpaid_clp": 0,
        "state": "pending",
        "provenance": null,
        "is_overdue": false,
        "status": "open",
        "disposition": null,
        "opened_on": "2026-09-22",
        "due_on": null,
        "closed_at": null,
        "closed_reason": null,
        "group_id": "a067dce6-0000-4000-8000-000000000002",
        "source": "staff",
        "document": null,
        "allocations": []
      }
    ],
    "payments": [
      {
        "id": "e86256b2-0000-4000-8000-000000000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "suggested_contact_id": null,
        "payer_reasons": [
          "payer_recorded_by_staff"
        ],
        "source": "manual",
        "provenance": "manual",
        "instrument": "transferencia",
        "amount_clp": 50000,
        "paid_on": "2026-09-24",
        "allocated_clp": 50000,
        "unallocated_clp": 0,
        "bank": "Banco de Chile",
        "account_number": null,
        "document_number": "884213",
        "operation_id": null,
        "note": "Abono de la primera cuota",
        "card_fee_bps": null,
        "card_surcharge_clp": null,
        "card_surcharge_source": null,
        "location_id": null,
        "bank_movement_id": null,
        "gateway_transaction_id": null,
        "recorded_by": null,
        "recorded_at": "2026-09-22T22:21:16.954Z",
        "reversed": false,
        "reversed_at": null,
        "reversal_reason": null,
        "allocations": [
          {
            "id": "e8df2087-0000-4000-8000-000000000001",
            "customer_payment_id": "e86256b2-0000-4000-8000-000000000001",
            "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
            "amount_clp": 50000,
            "method": "staff",
            "reasons": [
              "allocation_staff_manual"
            ],
            "allocated_by": null,
            "allocated_at": "2026-09-22T22:21:16.990Z",
            "released_at": null,
            "release_reason": null,
            "live": true
          }
        ]
      }
    ],
    "pending_verification": []
  }
}

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

GET /obligations

What is owed, by state, contact, kind or group

payments:read. ⚠ state MIXES A COLUMN AND A DERIVATION. cancelled and waived are stored; open is the stored status; pending, partial, paid, overpaid, overdue and overdue_partial are COMPUTED from allocations and exist nowhere in the schema — nothing sweeps them, so a late payment always finds a live obligation to settle. The server pushes down what SQL can answer and applies the derived half over a bounded window, reported as meta.derived_filter_window.

ParameterInTypeRequiredConstraints
contact_idqueryuuidno
statequerypending \partial \paid \
kindqueryappointment_deposit \service \pack \
group_idqueryuuidno
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 0, por defecto 0
curl https://api.vitrinadev.com/api/v1/obligations \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "b44936d0-0000-4000-8000-000000000004",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "kind": "service",
      "direction": "receivable",
      "label": "Ortodoncia — primera cuota",
      "expected_clp": 112000,
      "allocated_clp": 0,
      "outstanding_clp": 112000,
      "overpaid_clp": 0,
      "state": "pending",
      "provenance": null,
      "is_overdue": false,
      "status": "open",
      "disposition": null,
      "opened_on": "2026-09-22",
      "due_on": "2026-10-10",
      "closed_at": null,
      "closed_reason": null,
      "group_id": null,
      "source": "staff",
      "document": null,
      "allocations": []
    },
    {
      "id": "b44936d0-0000-4000-8000-000000000001",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "kind": "pack",
      "direction": "receivable",
      "label": "Pack 4 controles de ortodoncia",
      "expected_clp": 96000,
      "allocated_clp": 0,
      "outstanding_clp": 96000,
      "overpaid_clp": 0,
      "state": "pending",
      "provenance": null,
      "is_overdue": false,
      "status": "open",
      "disposition": null,
      "opened_on": "2026-09-22",
      "due_on": null,
      "closed_at": null,
      "closed_reason": null,
      "group_id": "a067dce6-0000-4000-8000-000000000001",
      "source": "staff",
      "document": null,
      "allocations": []
    },
    {
      "id": "b44936d0-0000-4000-8000-000000000002",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "kind": "service",
      "direction": "receivable",
      "label": "Control de ortodoncia",
      "expected_clp": 112000,
      "allocated_clp": 0,
      "outstanding_clp": 112000,
      "overpaid_clp": 0,
      "state": "pending",
      "provenance": null,
      "is_overdue": false,
      "status": "open",
      "disposition": null,
      "opened_on": "2026-09-22",
      "due_on": null,
      "closed_at": null,
      "closed_reason": null,
      "group_id": "a067dce6-0000-4000-8000-000000000002",
      "source": "staff",
      "document": null,
      "allocations": []
    }
  ],
  "meta": {
    "pagination": {
      "limit": 50,
      "offset": 0,
      "total": 4
    },
    "derived_filter_window": 500
  }
}

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

POST /obligations

Record what somebody owes

payments:write. The variable-amount case staff type by hand — "Tratamiento conducto, $80.000, vence en 7 días". label is FROZEN at creation (ADR 0057): renaming a servicio next month must not rewrite what this patient was quoted. The three document-anchored kinds require their document, and writing one twice for the same document yields ONE expectation — a partial unique index, not a convention.

Body

FieldTypeRequiredConstraints
contact_iduuidyes
kindappointment_deposit \service \pack \
expected_clpintegeryes≥ 1, ≤ 999999999999
labelstringyesmín. 1, máx. 280
opened_onstring—patrón ^\d{4}-\d{2}-\d{2}$
due_onstring | null—patrón ^\d{4}-\d{2}-\d{2}$
group_idstring | null—
appointment_idstring | null—
reservation_idstring | null—
sale_note_idstring | null—
curl -X POST https://api.vitrinadev.com/api/v1/obligations \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "service",
    "expected_clp": 112000,
    "label": "Ortodoncia — primera cuota",
    "due_on": "2026-10-10"
  }'

Example response (201)

{
  "data": {
    "id": "b44936d0-0000-4000-8000-000000000004",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "service",
    "direction": "receivable",
    "label": "Ortodoncia — primera cuota",
    "expected_clp": 112000,
    "allocated_clp": 0,
    "outstanding_clp": 112000,
    "overpaid_clp": 0,
    "state": "pending",
    "provenance": null,
    "is_overdue": false,
    "status": "open",
    "disposition": null,
    "opened_on": "2026-09-22",
    "due_on": "2026-10-10",
    "closed_at": null,
    "closed_reason": null,
    "group_id": null,
    "source": "staff",
    "document": null,
    "allocations": []
  }
}

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

GET /obligations/{id}

One cobro, its derived state and its allocation trail

payments:read. state and outstanding_clp are computed on read; provenance rides alongside the state so the badge keeps ADR 0086's split after the rename of verified to paid — green means a bank said so, violet means a human did, and one hand-asserted peso in the total grades the whole figure down to manual.

ParameterInTypeRequiredConstraints
idpathuuidyes
todayquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
curl https://api.vitrinadev.com/api/v1/obligations/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "b44936d0-0000-4000-8000-000000000004",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "service",
    "direction": "receivable",
    "label": "Ortodoncia — primera cuota",
    "expected_clp": 112000,
    "allocated_clp": 0,
    "outstanding_clp": 112000,
    "overpaid_clp": 0,
    "state": "pending",
    "provenance": null,
    "is_overdue": false,
    "status": "open",
    "disposition": null,
    "opened_on": "2026-09-22",
    "due_on": "2026-10-10",
    "closed_at": null,
    "closed_reason": null,
    "group_id": null,
    "source": "staff",
    "document": null,
    "allocations": []
  }
}

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

PATCH /obligations/{id}

Adjust what is owed, its deadline or its label — with a reason

payments:write. ⚠ expected_clp IS MUTABLE AND THAT IS CORRECT: the clinic quoted $15.000 for the evaluación, the patient chose two sessions, the expectation becomes $80.000 — and the allocations already made are untouched and still valid, because only the DERIVED outstanding changes. What must not happen is silently: reason is required and the change writes audit_log (obligations.adjust) in the SAME transaction, so a crash cannot separate the amount from why it moved. A closed cobro is a 409: its amount and its plazo are history.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
expected_clpinteger—≥ 1, ≤ 999999999999
due_onstring | null—patrón ^\d{4}-\d{2}-\d{2}$
labelstring—mín. 1, máx. 280
reasonstringyesmín. 1, máx. 280
curl -X PATCH https://api.vitrinadev.com/api/v1/obligations/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_clp": 108000,
    "reason": "Se aplicó el descuento del convenio"
  }'

Example response (200)

{
  "data": {
    "id": "b44936d0-0000-4000-8000-000000000004",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "service",
    "direction": "receivable",
    "label": "Ortodoncia — primera cuota",
    "expected_clp": 108000,
    "allocated_clp": 0,
    "outstanding_clp": 108000,
    "overpaid_clp": 0,
    "state": "pending",
    "provenance": null,
    "is_overdue": false,
    "status": "open",
    "disposition": null,
    "opened_on": "2026-09-22",
    "due_on": "2026-10-10",
    "closed_at": null,
    "closed_reason": null,
    "group_id": null,
    "source": "staff",
    "document": null,
    "allocations": []
  }
}

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

POST /obligations/{id}/cancel

Call off a cobro, and say what became of the money

payments:write. disposition is BR-266/267's vocabulary verbatim (aplicado | devuelto | perdido) and is ORTHOGONAL to the status: a cancelled reserva whose seña was KEPT is status: cancelled + disposition: perdido. Collapsing the two is how a dealership loses track of money it decided to keep. 409 when the cobro is already closed.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
reasonstringyesmín. 1, máx. 280
dispositionaplicado \devuelto \perdido
curl -X POST https://api.vitrinadev.com/api/v1/obligations/<id>/cancel \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "La prestación no se realizó",
    "disposition": "devuelto"
  }'

Example response (200)

{
  "data": {
    "id": "4fd0653c-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "service",
    "direction": "receivable",
    "label": "Control anulado",
    "expected_clp": 9000,
    "allocated_clp": 0,
    "outstanding_clp": 0,
    "overpaid_clp": 0,
    "state": "cancelled",
    "provenance": null,
    "is_overdue": false,
    "status": "cancelled",
    "disposition": "devuelto",
    "opened_on": "2026-09-22",
    "due_on": null,
    "closed_at": "2026-09-22T22:21:17.176Z",
    "closed_reason": "La prestación no se realizó",
    "group_id": null,
    "source": "staff",
    "document": null,
    "allocations": []
  }
}

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

POST /obligations/{id}/waive

Forgive what is owed, with a reason

payments:write. One of the three facts allocations cannot express, which is why status is a column at all. It does NOT release the pesos already applied: money that was applied was applied, the derivation stops counting the line as outstanding, and moving those pesos elsewhere is a release — its own verb, its own reason. 409 when the cobro is already closed.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
reasonstringyesmín. 1, máx. 280
curl -X POST https://api.vitrinadev.com/api/v1/obligations/<id>/waive \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Cortesía de la clínica por la espera"
  }'

Example response (200)

{
  "data": {
    "id": "03e03646-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "service",
    "direction": "receivable",
    "label": "Control extra",
    "expected_clp": 12000,
    "allocated_clp": 0,
    "outstanding_clp": 0,
    "overpaid_clp": 0,
    "state": "waived",
    "provenance": null,
    "is_overdue": false,
    "status": "waived",
    "disposition": null,
    "opened_on": "2026-09-22",
    "due_on": null,
    "closed_at": "2026-09-22T22:21:17.162Z",
    "closed_reason": "Cortesía de la clínica por la espera",
    "group_id": null,
    "source": "staff",
    "document": null,
    "allocations": []
  }
}

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

GET /obligations/groups/{groupId}

A presupuesto, pack or plan and every line in it

payments:read. The group carries NO total: expected_clp, allocated_clp, outstanding_clp and the headline state are derived from its lines every time they are asked for, because a stored total is BR-249's defect with a friendlier name. Lines come back in the order money fills them (due_on ascending, deadline-less last).

ParameterInTypeRequiredConstraints
groupIdpathuuidyes
todayquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
curl https://api.vitrinadev.com/api/v1/obligations/groups/<id> \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": {
    "id": "a067dce6-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "kind": "pack",
    "label": "Pack 4 controles de ortodoncia",
    "status": "open",
    "obligation_count": 1,
    "expected_clp": 96000,
    "allocated_clp": 0,
    "outstanding_clp": 96000,
    "state": "pending",
    "obligations": [
      {
        "id": "b44936d0-0000-4000-8000-000000000001",
        "contact_id": "1a73af9e-0000-4000-8000-000000000001",
        "kind": "pack",
        "direction": "receivable",
        "label": "Pack 4 controles de ortodoncia",
        "expected_clp": 96000,
        "allocated_clp": 0,
        "outstanding_clp": 96000,
        "overpaid_clp": 0,
        "state": "pending",
        "provenance": null,
        "is_overdue": false,
        "status": "open",
        "disposition": null,
        "opened_on": "2026-09-22",
        "due_on": null,
        "closed_at": null,
        "closed_reason": null,
        "group_id": "a067dce6-0000-4000-8000-000000000001",
        "source": "staff",
        "document": null,
        "allocations": []
      }
    ],
    "created_at": "2026-09-22T22:21:14.760Z"
  }
}

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

GET /payments

Money that arrived — the Transferencias queue, or one contact

payments:read. Without filters, the tenant's recent payments newest first. unapplied=true is the Transferencias queue: Σ live allocations &lt; amount_clp, i.e. money we hold that is not fully spoken for — it covers all three shapes staff sees (no payer, a payer and no allocation, partly applied with a remainder), and unidentified=true narrows it to the first. Every row carries unallocated_clp, suggested_contact_id and payer_reasons (the matcher's stable codes, rendered as chips) plus its full allocation trail, released rows included — the Historial tab draws payment → allocations → obligations with no request per row. Reversed money is EXCLUDED unless include_reversed=true: reversed money is not money we hold.

ParameterInTypeRequiredConstraints
contact_idqueryuuidno
unappliedqueryanyno
unidentifiedqueryanyno
include_reversedqueryanyno
fromquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
toquerystringnopatrón ^\d{4}-\d{2}-\d{2}$
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 0, por defecto 0
curl https://api.vitrinadev.com/api/v1/payments \
  -H "Authorization: Bearer $VITRINA_KEY"

Example response (200)

{
  "data": [
    {
      "id": "e86256b2-0000-4000-8000-000000000001",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "suggested_contact_id": null,
      "payer_reasons": [
        "payer_recorded_by_staff"
      ],
      "source": "manual",
      "provenance": "manual",
      "instrument": "transferencia",
      "amount_clp": 50000,
      "paid_on": "2026-09-24",
      "allocated_clp": 50000,
      "unallocated_clp": 0,
      "bank": "Banco de Chile",
      "account_number": null,
      "document_number": "884213",
      "operation_id": null,
      "note": "Abono de la primera cuota",
      "card_fee_bps": null,
      "card_surcharge_clp": null,
      "card_surcharge_source": null,
      "location_id": null,
      "bank_movement_id": null,
      "gateway_transaction_id": null,
      "recorded_by": null,
      "recorded_at": "2026-09-22T22:21:16.954Z",
      "reversed": false,
      "reversed_at": null,
      "reversal_reason": null,
      "allocations": [
        {
          "id": "e8df2087-0000-4000-8000-000000000001",
          "customer_payment_id": "e86256b2-0000-4000-8000-000000000001",
          "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
          "amount_clp": 50000,
          "method": "staff",
          "reasons": [
            "allocation_staff_manual"
          ],
          "allocated_by": null,
          "allocated_at": "2026-09-22T22:21:16.990Z",
          "released_at": null,
          "release_reason": null,
          "live": true
        }
      ]
    }
  ],
  "meta": {
    "pagination": {
      "limit": 50,
      "offset": 0,
      "total": 1
    }
  }
}

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

POST /payments

Record money by hand: cash, a card, a bono, a cheque

payments:write. Credits the ledger with provenance: "manual" — ADR 0086's distinction as a value: a human said so. verified means A BANK SAID SO and is unreachable through this door by construction (the database binds it to a comprobante or a gateway transaction).

WHAT IT IS FOR, in precedence order: an explicit allocations split is applied VERBATIM; obligation_id fills that one cobro up to its outstanding; otherwise the planner proposes the same split the matcher would have applied, and auto_allocate: false credits the money and applies nothing. Anything left over stays the contact's CREDIT — a first-class outcome, never an error. contact_id may be null: money whose payer is not yet known is recorded, not refused.

THE CARD RECARGO IS RESOLVED SERVER-SIDE. On a tarjeta_debito / tarjeta_credito payment the workspace’s configured rate for THAT card type (tenant.settings.card_fees — débito ≠ crédito) is applied and FROZEN on the row as card_fee_bps + card_surcharge_clp + card_surcharge_source: "calculado", and the response carries all three. Send neither field and that is what you get; send card_fee_bps to override the RATE for one swipe (0 is a rate and means a $0 recargo, never "unset"); send card_surcharge_clp to override the FIGURE, which freezes "ajustado". A workspace with no rate configured for that card type — and the legacy tarjeta member, whose card type nobody recorded — records NO recargo at all, never a $0 one. ⚠ THE RECARGO IS WHAT THE CUSTOMER PAYS ON TOP: it is NOT part of amount_clp and is allocated to nothing.

⚠ Idempotency-Key IS STORED ON THE ROW, not merely cached: a repeat returns 200 with the existing payment and meta.replayed: true instead of 409ing, because a receptionist who clicked twice asked one question. A first write answers 201. 409 when the split over-allocates the payment (error.details.unallocated_clp says what was left), when the cobro is closed, or when it belongs to somebody else.

Body

FieldTypeRequiredConstraints
contact_idstring | null—
sourcemanual \card_terminal \credit_note
instrumentefectivo \transferencia \cheque \
amount_clpintegeryes≥ 1, ≤ 999999999999
paid_onstringyespatrón ^\d{4}-\d{2}-\d{2}$
bankstring | null—máx. 120
account_numberstring | null—máx. 120
document_numberstring | null—máx. 120
notestring | null—máx. 2000
card_fee_bpsinteger | null—≥ 0, ≤ 10000
card_surcharge_clpinteger | null—≥ 0, ≤ 999999999999
location_idstring | null—
allocationsobject[]—
obligation_idstring | null—
auto_allocateboolean—
curl -X POST https://api.vitrinadev.com/api/v1/payments \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "instrument": "transferencia",
    "amount_clp": 50000,
    "paid_on": "2026-09-24",
    "bank": "Banco de Chile",
    "document_number": "884213",
    "note": "Abono de la primera cuota",
    "allocations": [
      {
        "obligation_id": "b44936d0-0000-4000-8000-000000000004",
        "amount_clp": 50000
      }
    ]
  }'

Example response (201)

{
  "data": {
    "payment": {
      "id": "e86256b2-0000-4000-8000-000000000001",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "suggested_contact_id": null,
      "payer_reasons": [
        "payer_recorded_by_staff"
      ],
      "source": "manual",
      "provenance": "manual",
      "instrument": "transferencia",
      "amount_clp": 50000,
      "paid_on": "2026-09-24",
      "allocated_clp": 50000,
      "unallocated_clp": 0,
      "bank": "Banco de Chile",
      "account_number": null,
      "document_number": "884213",
      "operation_id": null,
      "note": "Abono de la primera cuota",
      "card_fee_bps": null,
      "card_surcharge_clp": null,
      "card_surcharge_source": null,
      "location_id": null,
      "bank_movement_id": null,
      "gateway_transaction_id": null,
      "recorded_by": null,
      "recorded_at": "2026-09-22T22:21:16.954Z",
      "reversed": false,
      "reversed_at": null,
      "reversal_reason": null,
      "allocations": [
        {
          "id": "e8df2087-0000-4000-8000-000000000001",
          "customer_payment_id": "e86256b2-0000-4000-8000-000000000001",
          "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
          "amount_clp": 50000,
          "method": "staff",
          "reasons": [
            "allocation_staff_manual"
          ],
          "allocated_by": null,
          "allocated_at": "2026-09-22T22:21:16.990Z",
          "released_at": null,
          "release_reason": null,
          "live": true
        }
      ]
    },
    "applied": [
      {
        "obligationId": "b44936d0-0000-4000-8000-000000000004",
        "amountClp": 50000,
        "allocatedClp": 50000,
        "outstandingClp": 58000,
        "expectedClp": 108000,
        "state": "partial",
        "label": "Ortodoncia — primera cuota"
      }
    ],
    "surplus_clp": 0,
    "reasons": [
      "payer_recorded_by_staff",
      "allocation_staff_manual"
    ]
  }
}

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

GET /payments/{id}

One payment with its full allocation trail

payments:read. Released allocations are INCLUDED and flagged live: false: the trail is the readable history of where a peso WAS applied, which is the question a dispute months later actually asks.

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

Example response (200)

{
  "data": {
    "id": "e86256b2-0000-4000-8000-000000000001",
    "contact_id": "1a73af9e-0000-4000-8000-000000000001",
    "suggested_contact_id": null,
    "payer_reasons": [
      "payer_recorded_by_staff"
    ],
    "source": "manual",
    "provenance": "manual",
    "instrument": "transferencia",
    "amount_clp": 50000,
    "paid_on": "2026-09-24",
    "allocated_clp": 50000,
    "unallocated_clp": 0,
    "bank": "Banco de Chile",
    "account_number": null,
    "document_number": "884213",
    "operation_id": null,
    "note": "Abono de la primera cuota",
    "card_fee_bps": null,
    "card_surcharge_clp": null,
    "card_surcharge_source": null,
    "location_id": null,
    "bank_movement_id": null,
    "gateway_transaction_id": null,
    "recorded_by": null,
    "recorded_at": "2026-09-22T22:21:16.954Z",
    "reversed": false,
    "reversed_at": null,
    "reversal_reason": null,
    "allocations": [
      {
        "id": "e8df2087-0000-4000-8000-000000000001",
        "customer_payment_id": "e86256b2-0000-4000-8000-000000000001",
        "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
        "amount_clp": 50000,
        "method": "staff",
        "reasons": [
          "allocation_staff_manual"
        ],
        "allocated_by": null,
        "allocated_at": "2026-09-22T22:21:16.990Z",
        "released_at": null,
        "release_reason": null,
        "live": true
      }
    ]
  }
}

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

POST /payments/{id}/allocations

Allocate — or re-allocate — a payment across obligations

payments:write. Writes one payment_allocation row per line, with method: "staff". Passing release makes it a RE-allocation: the named rows are released FIRST, in the same transaction, because nothing is ever edited in place — a split is changed by releasing and writing anew, so the history stays readable.

409 over_allocation when the lines exceed what is left on the payment (error.details.unallocated_clp carries the figure, so the screen can say «solo quedan $5.000 sin asignar»), obligation_not_open, contact_mismatch (one person's money never settles another's debt — a row Postgres refuses, not a rule a service remembers), payment_reversed, payer_unknown. Re-sending an identical split is a no-op, not a double-allocation.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
allocationsobject[]yes
releaseobject—
curl -X POST https://api.vitrinadev.com/api/v1/payments/<id>/allocations \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "allocations": [
      {
        "obligation_id": "b44936d0-0000-4000-8000-000000000004",
        "amount_clp": 20000
      }
    ]
  }'

Example response (200)

{
  "data": {
    "payment": {
      "id": "e86256b2-0000-4000-8000-000000000002",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "suggested_contact_id": null,
      "payer_reasons": [
        "payer_recorded_by_staff"
      ],
      "source": "manual",
      "provenance": "manual",
      "instrument": "efectivo",
      "amount_clp": 20000,
      "paid_on": "2026-09-24",
      "allocated_clp": 20000,
      "unallocated_clp": 0,
      "bank": null,
      "account_number": null,
      "document_number": null,
      "operation_id": null,
      "note": null,
      "card_fee_bps": null,
      "card_surcharge_clp": null,
      "card_surcharge_source": null,
      "location_id": "b1b1b1b1-0000-4000-8000-000000000002",
      "bank_movement_id": null,
      "gateway_transaction_id": null,
      "recorded_by": null,
      "recorded_at": "2026-09-22T22:21:17.052Z",
      "reversed": false,
      "reversed_at": null,
      "reversal_reason": null,
      "allocations": [
        {
          "id": "e8df2087-0000-4000-8000-000000000002",
          "customer_payment_id": "e86256b2-0000-4000-8000-000000000002",
          "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
          "amount_clp": 20000,
          "method": "staff",
          "reasons": [
            "allocation_staff_accepted_plan",
            "allocation_partial"
          ],
          "allocated_by": null,
          "allocated_at": "2026-09-22T22:21:17.057Z",
          "released_at": null,
          "release_reason": null,
          "live": true
        }
      ]
    },
    "applied": [
      {
        "obligationId": "b44936d0-0000-4000-8000-000000000004",
        "amountClp": 20000,
        "allocatedClp": 90000,
        "outstandingClp": 18000,
        "expectedClp": 108000,
        "state": "partial",
        "label": "Ortodoncia — primera cuota"
      }
    ],
    "surplus_clp": 0,
    "reasons": [
      "allocation_staff_manual"
    ]
  }
}

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

DELETE /payments/{id}/allocations/{allocationId}

Release one allocation, with a reason

payments:write. Stamps released_at / released_by / release_reason; the row SURVIVES. reason is required (the table requires it) — money un-applied with no stated reason hands the next person the same question with no more information than the first person had. 409 already_released, 404 for an allocation that belongs to another payment (never a 403, which would confirm it exists).

ParameterInTypeRequiredConstraints
idpathuuidyes
allocationIdpathuuidyes

Body

FieldTypeRequiredConstraints
reasonstringyesmín. 1, máx. 280
curl -X DELETE https://api.vitrinadev.com/api/v1/payments/<id>/allocations/<id> \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Se imputó a la obligación equivocada"
  }'

Example response (200)

{
  "data": {
    "payment": {
      "id": "e86256b2-0000-4000-8000-000000000002",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "suggested_contact_id": null,
      "payer_reasons": [
        "payer_recorded_by_staff"
      ],
      "source": "manual",
      "provenance": "manual",
      "instrument": "efectivo",
      "amount_clp": 20000,
      "paid_on": "2026-09-24",
      "allocated_clp": 0,
      "unallocated_clp": 20000,
      "bank": null,
      "account_number": null,
      "document_number": null,
      "operation_id": null,
      "note": null,
      "card_fee_bps": null,
      "card_surcharge_clp": null,
      "card_surcharge_source": null,
      "location_id": "b1b1b1b1-0000-4000-8000-000000000002",
      "bank_movement_id": null,
      "gateway_transaction_id": null,
      "recorded_by": null,
      "recorded_at": "2026-09-22T22:21:17.052Z",
      "reversed": false,
      "reversed_at": null,
      "reversal_reason": null,
      "allocations": [
        {
          "id": "e8df2087-0000-4000-8000-000000000002",
          "customer_payment_id": "e86256b2-0000-4000-8000-000000000002",
          "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
          "amount_clp": 20000,
          "method": "staff",
          "reasons": [
            "allocation_staff_accepted_plan",
            "allocation_partial"
          ],
          "allocated_by": null,
          "allocated_at": "2026-09-22T22:21:17.057Z",
          "released_at": "2026-09-22T22:21:17.094Z",
          "release_reason": "Se imputó a la obligación equivocada",
          "live": false
        }
      ]
    },
    "released": {
      "id": "e8df2087-0000-4000-8000-000000000002",
      "customer_payment_id": "e86256b2-0000-4000-8000-000000000002",
      "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
      "amount_clp": 20000,
      "method": "staff",
      "reasons": [
        "allocation_staff_accepted_plan",
        "allocation_partial"
      ],
      "allocated_by": null,
      "allocated_at": "2026-09-22T22:21:17.057Z",
      "released_at": "2026-09-22T22:21:17.094Z",
      "release_reason": "Se imputó a la obligación equivocada",
      "live": false
    }
  }
}

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

POST /payments/{id}/assign-payer

Say whose money a payment is (or take the name off it)

payments:write. The one field of a payment that is ever edited (§2.2): everything else about a received amount is immutable, and a wrong amount is a reversal plus a new row.

⚠ RE-IDENTIFYING A PAYER THAT ALREADY HAS LIVE ALLOCATIONS RELEASES THEM FIRST, and the response says which. The identity FK cascades contact_id onto the allocations, and dragging them onto a new person while their obligations still belong to the old one is a row Postgres refuses — releasing states the truth instead. contact_id: null puts the money back in the unidentified queue (the reject half of a suggestion).

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
contact_idstring | nullyes
curl -X POST https://api.vitrinadev.com/api/v1/payments/<id>/assign-payer \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "1a73af9e-0000-4000-8000-000000000001"
  }'

Example response (200)

{
  "data": {
    "payment": {
      "id": "e86256b2-0000-4000-8000-000000000002",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "suggested_contact_id": null,
      "payer_reasons": [
        "payer_assigned_by_staff"
      ],
      "source": "manual",
      "provenance": "manual",
      "instrument": "efectivo",
      "amount_clp": 20000,
      "paid_on": "2026-09-24",
      "allocated_clp": 0,
      "unallocated_clp": 20000,
      "bank": null,
      "account_number": null,
      "document_number": null,
      "operation_id": null,
      "note": null,
      "card_fee_bps": null,
      "card_surcharge_clp": null,
      "card_surcharge_source": null,
      "location_id": "b1b1b1b1-0000-4000-8000-000000000002",
      "bank_movement_id": null,
      "gateway_transaction_id": null,
      "recorded_by": null,
      "recorded_at": "2026-09-22T22:21:17.052Z",
      "reversed": false,
      "reversed_at": null,
      "reversal_reason": null,
      "allocations": [
        {
          "id": "e8df2087-0000-4000-8000-000000000002",
          "customer_payment_id": "e86256b2-0000-4000-8000-000000000002",
          "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
          "amount_clp": 20000,
          "method": "staff",
          "reasons": [
            "allocation_staff_accepted_plan",
            "allocation_partial"
          ],
          "allocated_by": null,
          "allocated_at": "2026-09-22T22:21:17.057Z",
          "released_at": "2026-09-22T22:21:17.094Z",
          "release_reason": "Se imputó a la obligación equivocada",
          "live": false
        }
      ]
    },
    "released": []
  }
}

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

POST /payments/{id}/reverse

Reverse a payment — the trio, and every allocation released

⚠ payments:reverse, THE ONLY ROUTE ON THAT SCOPE. A reversal is the one act here that makes money the platform already told a customer about stop existing, and a workspace must be able to let somebody record money without letting them erase it.

Writes reversed_at / reversed_by / reversal_reason AND releases every live allocation, in ONE transaction: two statements that could commit separately would leave a reversed payment still settling obligations. ⚠ IT DOES NOT RE-IMPOSE THE awaiting_payment BLOCK (§5.1, open question 9.3): the customer was already told the money arrived, and the platform has no honest sentence for "we un-saw your transfer". 409 already_reversed — reversing twice never rewrites the first reason.

ParameterInTypeRequiredConstraints
idpathuuidyes

Body

FieldTypeRequiredConstraints
reasonstringyesmín. 1, máx. 280
curl -X POST https://api.vitrinadev.com/api/v1/payments/<id>/reverse \
  -H "Authorization: Bearer $VITRINA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "El depósito fue rechazado por el banco"
  }'

Example response (200)

{
  "data": {
    "payment": {
      "id": "e86256b2-0000-4000-8000-000000000002",
      "contact_id": "1a73af9e-0000-4000-8000-000000000001",
      "suggested_contact_id": null,
      "payer_reasons": [
        "payer_assigned_by_staff"
      ],
      "source": "manual",
      "provenance": "manual",
      "instrument": "efectivo",
      "amount_clp": 20000,
      "paid_on": "2026-09-24",
      "allocated_clp": 0,
      "unallocated_clp": 20000,
      "bank": null,
      "account_number": null,
      "document_number": null,
      "operation_id": null,
      "note": null,
      "card_fee_bps": null,
      "card_surcharge_clp": null,
      "card_surcharge_source": null,
      "location_id": "b1b1b1b1-0000-4000-8000-000000000002",
      "bank_movement_id": null,
      "gateway_transaction_id": null,
      "recorded_by": null,
      "recorded_at": "2026-09-22T22:21:17.052Z",
      "reversed": true,
      "reversed_at": "2026-09-22T22:21:17.114Z",
      "reversal_reason": "El depósito fue rechazado por el banco",
      "allocations": [
        {
          "id": "e8df2087-0000-4000-8000-000000000002",
          "customer_payment_id": "e86256b2-0000-4000-8000-000000000002",
          "customer_obligation_id": "b44936d0-0000-4000-8000-000000000004",
          "amount_clp": 20000,
          "method": "staff",
          "reasons": [
            "allocation_staff_accepted_plan",
            "allocation_partial"
          ],
          "allocated_by": null,
          "allocated_at": "2026-09-22T22:21:17.057Z",
          "released_at": "2026-09-22T22:21:17.094Z",
          "release_reason": "Se imputó a la obligación equivocada",
          "live": false
        }
      ]
    },
    "released": []
  }
}

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

On this page