Cobros y pagos
El ledger del workspace: lo que se debe, lo que llegó y cómo se imputa lo segundo a lo primero.
Descarga la proyección completa de la API pública: openapi.json.
Cobrar y conciliar pagos explica este recurso en prosa, con ejemplos ejecutables.
| Método | Ruta | Qué hace |
|---|---|---|
GET | /contacts/{id}/ledger | Everything one person owes, has paid, and has in their favour |
GET | /installment-plans | List installment plans |
POST | /installment-plans | Create an installment plan |
GET | /installment-plans/{id} | Fetch one installment plan |
PATCH | /installment-plans/{id} | Rename an installment plan |
POST | /installment-plans/{id}/cancel | Cancel an installment plan |
POST | /installment-plans/import | Import cuotas in bulk (JSON) |
POST | /installment-plans/import-file | Import cuotas from a CSV file |
GET | /installments | List cuotas |
GET | /installments/{id} | Fetch one cuota |
PATCH | /installments/{id} | Change a cuota |
POST | /installments/{id}/mark-paid | Mark a cuota paid |
GET | /obligations | What is owed, by state, contact, kind or group |
POST | /obligations | Record 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}/cancel | Call off a cobro, and say what became of the money |
POST | /obligations/{id}/checkout | A Mercado Pago payment link for what one cobro still owes |
POST | /obligations/{id}/waive | Forgive what is owed, with a reason |
GET | /obligations/groups/{groupId} | A presupuesto, pack or plan and every line in it |
POST | /obligations/groups/{groupId}/checkout | A Mercado Pago payment link for a presupuesto, pack or plan |
POST | /payment-links | Send a customer a Mercado Pago link for one debt |
GET | /payment-links/options | What a customer owes, and whether a payment link can be sent |
GET | /payments | Money that arrived — the Transferencias queue, or one contact |
POST | /payments | Record money by hand: cash, a card, a bono, a cheque |
GET | /payments/{id} | One payment with its full allocation trail |
POST | /payments/{id}/allocations | Allocate — or re-allocate — a payment across obligations |
DELETE | /payments/{id}/allocations/{allocationId} | Release one allocation, with a reason |
POST | /payments/{id}/assign-payer | Say whose money a payment is (or take the name off it) |
POST | /payments/{id}/reverse | Reverse a payment — the trio, and every allocation released |
GET | /payments/instrument-fields | Which fields each payment instrument requires |
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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
today | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
curl https://api.vitrinadev.com/api/v1/contacts/<id>/ledger \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /installment-plans
List installment plans
payments:read. Plans newest first, each with its cuotas. Filter by contact_id or external_ref.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contact_id | query | uuid | no | |
external_ref | query | string | no | máx. 120 |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
offset | query | integer | null | no | ≥ 0, por defecto 0 |
curl https://api.vitrinadev.com/api/v1/installment-plans \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
[
{
"id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"label": "Plan de cuotas",
"external_ref": "CONTRATO-2291",
"currency": "CLP",
"count": 6,
"total_amount": 510000,
"paid_amount": 85000,
"outstanding": 425000,
"status": "active",
"installments": [
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}
],
"created_at": "2026-10-05T14:00:00.000Z"
}
]Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /installment-plans
Create an installment plan
payments:write. A customer owes money in cuotas. Send the debtor (contact_id, or rut / phone / email to find or create them) and either the cuotas (installments: amount, due date, optional own payment_link_url) or an even schedule (total_amount, count, first_due_date: monthly, the last cuota absorbs the rounding). external_ref is your own reference (contract, loan): it must be unique (409 otherwise) and is what the import matches on. Amounts are whole CLP. A plan is an obligation group on the payments ledger and each cuota one of its obligations: paid and overdue are derived from payments, never set. Nothing here sends a message: a debtor who is not a contact yet is created silently (no new_contact trigger).
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
contact_id | uuid | — | |
name | string | — | máx. 200 |
phone | string | — | máx. 40 |
email | string | — | máx. 200 |
rut | string | — | máx. 20 |
label | string | — | mín. 1, máx. 280 |
external_ref | string | — | mín. 1, máx. 120 |
installments | object[] | — | |
schedule | object | — |
curl -X POST https://api.vitrinadev.com/api/v1/installment-plans \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"rut": "12.345.678-5",
"name": "Marta Rojas",
"external_ref": "CONTRATO-2291",
"schedule": {
"total_amount": 510000,
"count": 6,
"first_due_date": "2026-10-05"
}
}'Ejemplo de respuesta (201)
{
"id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"label": "Plan de cuotas",
"external_ref": "CONTRATO-2291",
"currency": "CLP",
"count": 6,
"total_amount": 510000,
"paid_amount": 85000,
"outstanding": 425000,
"status": "active",
"installments": [
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}
],
"created_at": "2026-10-05T14:00:00.000Z"
}Responde: 201 · 400 · 401 · 403 · 404 · 409 · 429
GET /installment-plans/{id}
Fetch one installment plan
payments:read. The plan with every cuota, its status and what is still owed.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/installment-plans/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"label": "Plan de cuotas",
"external_ref": "CONTRATO-2291",
"currency": "CLP",
"count": 6,
"total_amount": 510000,
"paid_amount": 85000,
"outstanding": 425000,
"status": "active",
"installments": [
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}
],
"created_at": "2026-10-05T14:00:00.000Z"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /installment-plans/{id}
Rename an installment plan
payments:write. Changes the plan label.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
label | string | sí | mín. 1, máx. 280 |
curl -X PATCH https://api.vitrinadev.com/api/v1/installment-plans/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"label": "Plan matrícula 2027"
}'Ejemplo de respuesta (200)
{
"id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"label": "Plan de cuotas",
"external_ref": "CONTRATO-2291",
"currency": "CLP",
"count": 6,
"total_amount": 510000,
"paid_amount": 85000,
"outstanding": 425000,
"status": "active",
"installments": [
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}
],
"created_at": "2026-10-05T14:00:00.000Z"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /installment-plans/{id}/cancel
Cancel an installment plan
payments:write. Cancels every cuota still owing (money already paid stays) and ends the person's collection sequence. Needs a reason.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mín. 1, máx. 500 |
curl -X POST https://api.vitrinadev.com/api/v1/installment-plans/<id>/cancel \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Se refinanció la deuda"
}'Ejemplo de respuesta (200)
{
"id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"label": "Plan de cuotas",
"external_ref": "CONTRATO-2291",
"currency": "CLP",
"count": 6,
"total_amount": 510000,
"paid_amount": 85000,
"outstanding": 425000,
"status": "active",
"installments": [
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}
],
"created_at": "2026-10-05T14:00:00.000Z"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /installment-plans/import
Import cuotas in bulk (JSON)
payments:write. One row per cuota (external_ref, number, amount, due_date, and the debtor as contact_id / rut / phone / email + name; optional count, payment_link_url, label, paid_on). Rows sharing an external_ref are one plan. Idempotent on external_ref: the same rows again change nothing; a new amount, date or link updates a cuota that is still owing; paid_on marks it paid; a paid cuota never moves. Up to 2000 rows. Row problems are listed in errors and do not stop the rest. Nothing here sends a message: a debtor who is not a contact yet is created silently (no new_contact trigger).
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
rows | object[] | sí |
curl -X POST https://api.vitrinadev.com/api/v1/installment-plans/import \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{
"external_ref": "CONTRATO-2291",
"number": 1,
"count": 6,
"amount": 85000,
"due_date": "2026-10-05",
"rut": "12.345.678-5",
"name": "Marta Rojas",
"paid_on": "2026-10-04"
},
{
"external_ref": "CONTRATO-2291",
"number": 2,
"amount": 85000,
"due_date": "2026-11-05",
"rut": "12.345.678-5"
}
]
}'Ejemplo de respuesta (200)
{
"total_rows": 6,
"plans_created": 1,
"plans_updated": 0,
"installments_created": 6,
"installments_updated": 0,
"installments_unchanged": 0,
"marked_paid": 1,
"contacts_created": 1,
"errors": []
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /installment-plans/import-file
Import cuotas from a CSV file
payments:write. The same import as POST /installment-plans/import, from a CSV (file, up to 5 MB). Headers (Spanish or English): external_ref/referencia, number/cuota, amount/monto, due_date/vencimiento, and optional count, rut, phone/telefono, email/correo, name/nombre, contact_id, payment_link_url/link_pago, label, paid_on/pagado_el. Comma or semicolon separated; pesos as $85.000; dates as 2026-11-05 or 05/11/2026. Nothing here sends a message: a debtor who is not a contact yet is created silently (no new_contact trigger).
Or a JSON body (application/json) — what an MCP connector sends through call_operation, which cannot build multipart: \{ filename, content, content_encoding?, content_type? \} plus the operation's own form fields. content is UTF-8 text by default, or base64 when content_encoding is "base64" (images, PDF, audio, video). The same byte cap applies to the decoded bytes (413 above it); invalid base64 is a 400; the content type defaults from the filename extension.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
filename | string | sí | mín. 1, máx. 255 |
content | string | sí | mín. 1, máx. 35000000 |
content_encoding | utf8 \ | base64 | — |
content_type | string | — | máx. 200 |
curl -X POST https://api.vitrinadev.com/api/v1/installment-plans/import-file \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"file": "(binary CSV part)"
}'Ejemplo de respuesta (200)
{
"total_rows": 6,
"plans_created": 1,
"plans_updated": 0,
"installments_created": 6,
"installments_updated": 0,
"installments_unchanged": 0,
"marked_paid": 1,
"contacts_created": 1,
"errors": []
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /installments
List cuotas
payments:read. Cuotas across plans. status is pending, paid, overdue (past the due day and still owing) or cancelled; it is derived from payments, so filtering on it reads a bounded window of 500. Filter by contact_id or plan_id.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contact_id | query | uuid | no | |
plan_id | query | uuid | no | |
status | query | pending \ | paid \ | overdue \ |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
offset | query | integer | null | no | ≥ 0, por defecto 0 |
curl https://api.vitrinadev.com/api/v1/installments \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
[
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}
]Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /installments/{id}
Fetch one cuota
payments:read. One cuota with its status, what is owed and its payment link.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/installments/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
PATCH /installments/{id}
Change a cuota
payments:write. Amount, due date and/or the payment link. Amount and date only while the cuota is open; changes are audited with reason. payment_link_url: null removes the link.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
amount | integer | — | ≥ 1, ≤ 999999999999 |
due_date | string | — | patrón ^\d{4}-\d{2}-\d{2}$ |
payment_link_url | string | null | — | uri, máx. 2000, patrón ^https?:\/\/ |
reason | string | — | mín. 1, máx. 500 |
curl -X PATCH https://api.vitrinadev.com/api/v1/installments/<id> \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"due_date": "2026-11-20",
"reason": "Pidió más plazo"
}'Ejemplo de respuesta (200)
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "overdue",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /installments/{id}/mark-paid
Mark a cuota paid
payments:write. Records a payment for what is still owed on the cuota (instrument default otro, paid_on default today) and ends the person's collection sequence with the goal met. Already paid: nothing changes. A cancelled cuota answers 409.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
paid_on | string | — | patrón ^\d{4}-\d{2}-\d{2}$ |
instrument | efectivo \ | transferencia \ | cheque \ |
note | string | — | máx. 2000 |
curl -X POST https://api.vitrinadev.com/api/v1/installments/<id>/mark-paid \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"paid_on": "2026-11-09",
"instrument": "transferencia"
}'Ejemplo de respuesta (200)
{
"id": "0192f0b2-0000-7000-8000-00000000c001",
"plan_id": "0192f0b2-0000-7000-8000-00000000c000",
"contact_id": "0192f0b2-0000-7000-8000-00000000a001",
"number": 2,
"count": 6,
"amount": 85000,
"currency": "CLP",
"due_date": "2026-11-05",
"status": "paid",
"ledger_state": "overdue",
"paid_amount": 0,
"outstanding": 85000,
"paid_at": null,
"days_overdue": 4,
"payment_link_url": "https://pagos.ejemplo.cl/c/8842"
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contact_id | query | uuid | no | |
state | query | pending \ | partial \ | paid \ |
kind | query | appointment_deposit \ | service \ | pack \ |
group_id | query | uuid | no | |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
offset | query | integer | null | no | ≥ 0, por defecto 0 |
curl https://api.vitrinadev.com/api/v1/obligations \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
}Responde: 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.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
contact_id | uuid | sí | |
kind | appointment_deposit \ | service \ | pack \ |
expected_clp | integer | sí | ≥ 1, ≤ 999999999999 |
label | string | sí | mín. 1, máx. 280 |
opened_on | string | — | patrón ^\d{4}-\d{2}-\d{2}$ |
due_on | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
group_id | string | null | — | |
appointment_id | string | null | — | |
reservation_id | string | null | — | |
sale_note_id | string | 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"
}'Ejemplo de respuesta (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": []
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
today | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
curl https://api.vitrinadev.com/api/v1/obligations/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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": []
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
expected_clp | integer | — | ≥ 1, ≤ 999999999999 |
due_on | string | null | — | patrón ^\d{4}-\d{2}-\d{2}$ |
label | string | — | mín. 1, máx. 280 |
reason | string | sí | mí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"
}'Ejemplo de respuesta (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": []
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mín. 1, máx. 280 |
disposition | aplicado \ | 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"
}'Ejemplo de respuesta (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": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /obligations/{id}/checkout
A Mercado Pago payment link for what one cobro still owes
payments:write. Creates (or returns the existing) Checkout Pro link from the business's OWN connected Mercado Pago account for this obligation's outstanding amount, for any kind (deposit, service, pack, product, nota de venta, imported balance). Calling again returns the same link while the amount still matches. The item title is generic (never the obligation label). Installments are offered only for a final charge at or above the business threshold (mp_installments_min_clp). When paid, the money is applied to this obligation automatically and the customer returns to receipt_url. 409 with details.reason: not_connected (no Mercado Pago account connected), nothing_outstanding / already_paid, not_open (closed, or its earlier link was cancelled), not_receivable, no_contact. 404 when it does not exist; 502 provider_unavailable when Mercado Pago could not be reached (retry).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|
curl -X POST https://api.vitrinadev.com/api/v1/obligations/<id>/checkout \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{}'Ejemplo de respuesta (200)
{
"data": {
"checkout_id": "0192f6a1-7c3e-7b10-9a54-3c1d2e4f5a60",
"checkout_url": "https://www.mercadopago.cl/checkout/v1/redirect?pref_id=123456789-abc",
"amount_clp": 45000,
"installments": false,
"receipt_url": "https://app.example.com/pago/0192f6a1-7c3e-7b10-9a54-3c1d2e4f5a60",
"obligation_id": "0192f6a0-11aa-7c22-8d33-4e5f60718293",
"group_id": null
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mí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"
}'Ejemplo de respuesta (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": []
}
}Responde: 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).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
groupId | path | uuid | sí | |
today | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
curl https://api.vitrinadev.com/api/v1/obligations/groups/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /obligations/groups/{groupId}/checkout
A Mercado Pago payment link for a presupuesto, pack or plan
payments:write. One link for a whole group, or for a chosen partial (amount_clp, e.g. a presupuesto's first instalment, 1..outstanding). When paid, the money fills the group's obligations in the ledger order (nearest due date first) and nothing outside the group. One open link per group: asking again with the same amount returns it, a different amount replaces it (the old link stops taking money). 409 with details.reason: not_connected (no Mercado Pago account connected), nothing_outstanding / already_paid, not_open (closed, or its earlier link was cancelled), not_receivable, no_contact. 404 when it does not exist; 502 provider_unavailable when Mercado Pago could not be reached (retry). 400 amount_out_of_range when amount_clp exceeds the outstanding.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
groupId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
amount_clp | integer | — | > 0 |
curl -X POST https://api.vitrinadev.com/api/v1/obligations/groups/<id>/checkout \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount_clp": 150000
}'Ejemplo de respuesta (200)
{
"data": {
"checkout_id": "0192f6a1-7c3e-7b10-9a54-3c1d2e4f5a60",
"checkout_url": "https://www.mercadopago.cl/checkout/v1/redirect?pref_id=123456789-abc",
"amount_clp": 150000,
"installments": true,
"receipt_url": "https://app.example.com/pago/0192f6a1-7c3e-7b10-9a54-3c1d2e4f5a60",
"obligation_id": null,
"group_id": "0192f6a0-22bb-7d33-8e44-5f60718293a4"
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
POST /payment-links
Send a customer a Mercado Pago link for one debt
payments:write (+ messages:send and conversations:write to deliver). Creates (or reuses) the link for one obligation (its outstanding) or one group (amount_clp, 1..outstanding) and sends a generic message with it through the conversation, as the caller. When the window is closed (WhatsApp 24 h; Instagram/Messenger 7 days for a person), nothing is sent: delivery.status = not_sent, delivery.reason = outside_window, and link.checkout_url + text are there to copy. deliver: false only creates the link. An API key or connected app passes the outbound policy (422 / 409, acknowledge; may answer pending_approval). 409 transfer_only when the business charges only by transfer; the checkout refusals of POST /obligations/\{id\}/checkout otherwise. Audited as payments.link_sent. Paid links settle onto the debt automatically.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
obligation_id | uuid | — | |
group_id | uuid | — | |
amount_clp | integer | — | > 0 |
conversation_id | uuid | — | |
deliver | boolean | — | |
acknowledge | string[] | — |
curl -X POST https://api.vitrinadev.com/api/v1/payment-links \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"group_id": "0192f6a0-22bb-7d33-8e44-5f60718293a4",
"amount_clp": 120000,
"conversation_id": "0192f6a0-3333-7000-8000-000000000002"
}'Ejemplo de respuesta (200)
{
"data": {
"link": {
"checkout_id": "0192f6a1-7c3e-7b10-9a54-3c1d2e4f5a60",
"checkout_url": "https://www.mercadopago.cl/checkout/v1/redirect?pref_id=123456789-abc",
"amount_clp": 120000,
"installments": false,
"receipt_url": "https://app.example.com/pago/0192f6a1-7c3e-7b10-9a54-3c1d2e4f5a60",
"obligation_id": null,
"group_id": "0192f6a0-22bb-7d33-8e44-5f60718293a4"
},
"text": "Hola Ana, te enviamos el link para pagar $120.000 de Clínica Ejemplo:\nhttps://www.mercadopago.cl/checkout/v1/redirect?pref_id=123456789-abc\n\nApenas se acredite el pago, queda registrado automáticamente.",
"delivery": {
"status": "sent",
"reason": null,
"message_id": "0192f6a0-4444-7000-8000-000000000003",
"conversation_id": "0192f6a0-3333-7000-8000-000000000002",
"channel": "whatsapp",
"window": "open"
}
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /payment-links/options
What a customer owes, and whether a payment link can be sent
payments:read. The customer's open receivable debt as payable targets, most urgent first: each presupuesto / pack / plan is ONE target that may be paid in part (partial_allowed), each cobro outside a group is charged exactly its outstanding. Per target: whether a link may be sent (link_allowed; link_refusal = transfer_only | not_connected) under the charging preference. delivery: the conversation the link would go through and its messaging window (open, human_agent = Instagram/Messenger 24 h–7 days, closed = copy the link instead).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contact_id | query | uuid | sí | |
conversation_id | query | uuid | no |
curl https://api.vitrinadev.com/api/v1/payment-links/options \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": {
"contact_id": "0192f6a0-0000-7000-8000-000000000001",
"connected": true,
"total_outstanding_clp": 365000,
"debts": [
{
"target": "group",
"id": "0192f6a0-22bb-7d33-8e44-5f60718293a4",
"kind": "presupuesto",
"label": "Presupuesto E-47",
"outstanding_clp": 320000,
"partial_allowed": true,
"charge_type": "final",
"due_on": null,
"is_overdue": false,
"open_checkout": null,
"link_allowed": true,
"link_refusal": null,
"charge_mode": "mp_first"
}
],
"delivery": {
"conversation_id": "0192f6a0-3333-7000-8000-000000000002",
"channel": "whatsapp",
"window": "open",
"can_send": true,
"reason": null
}
}
}Responde: 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 < 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
contact_id | query | uuid | no | |
unapplied | query | any | no | |
unidentified | query | any | no | |
include_reversed | query | any | no | |
from | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
to | query | string | no | patrón ^\d{4}-\d{2}-\d{2}$ |
limit | query | integer | no | ≥ 1, ≤ 200, por defecto 50 |
offset | query | integer | null | no | ≥ 0, por defecto 0 |
curl https://api.vitrinadev.com/api/v1/payments \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
}
}Responde: 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.
SAY WHAT SALE IT WAS (sale, optional). A back office that records one sale per call sends sale: \{ ref, place?, items? \}: ref is the sale’s id in YOUR system (two payments with the same ref are ONE sale, a deposit and its balance; different refs are different sales even minutes apart); place: "store" marks a counter sale whatever the instrument, so a transfer taken at the till is still a store sale for ad reporting; items ([\{ external_id, quantity?, unit_price_clp? \}], external_id = your stable item id) is what was sold, sent on to the ad platforms as the Purchase’s content_ids / contents. Nothing else reads it.
⚠ 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.
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
contact_id | string | null | — | |
source | manual \ | card_terminal \ | credit_note |
instrument | efectivo \ | transferencia \ | cheque \ |
amount_clp | integer | sí | ≥ 1, ≤ 999999999999 |
paid_on | string | sí | patrón ^\d{4}-\d{2}-\d{2}$ |
bank | string | null | — | máx. 120 |
account_number | string | null | — | máx. 120 |
document_number | string | null | — | máx. 120 |
note | string | null | — | máx. 2000 |
sale | object | null | — | |
card_fee_bps | integer | null | — | ≥ 0, ≤ 10000 |
card_surcharge_clp | integer | null | — | ≥ 0, ≤ 999999999999 |
location_id | string | null | — | |
allocations | object[] | — | |
obligation_id | string | null | — | |
auto_allocate | boolean | — |
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
}
]
}'Ejemplo de respuesta (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"
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
curl https://api.vitrinadev.com/api/v1/payments/<id> \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (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
}
]
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
allocations | object[] | sí | |
release | object | — |
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
}
]
}'Ejemplo de respuesta (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"
]
}
}Responde: 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).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí | |
allocationId | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mí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"
}'Ejemplo de respuesta (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
}
}
}Responde: 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).
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
contact_id | string | null | sí |
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"
}'Ejemplo de respuesta (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": []
}
}Responde: 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.
| Parámetro | En | Tipo | Obligatorio | Restricciones |
|---|---|---|---|---|
id | path | uuid | sí |
Cuerpo
| Campo | Tipo | Obligatorio | Restricciones |
|---|---|---|---|
reason | string | sí | mí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"
}'Ejemplo de respuesta (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": []
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
GET /payments/instrument-fields
Which fields each payment instrument requires
payments:read, sale_notes:write or reservations:write (any one). The BR-261/BR-447 table every payment guard reads — POST /payments, the funding of POST /sale-notes and the MCP tools refuse a payment whose required_fields are blank. A form reads it to count what is still missing before submitting, rather than keeping its own copy. Static vocabulary: the same for every workspace.
curl https://api.vitrinadev.com/api/v1/payments/instrument-fields \
-H "Authorization: Bearer $VITRINA_KEY"Ejemplo de respuesta (200)
{
"data": [
{
"instrument": "efectivo",
"required_fields": []
},
{
"instrument": "transferencia",
"required_fields": []
},
{
"instrument": "cheque",
"required_fields": [
"bank",
"account_number",
"document_number"
]
},
{
"instrument": "vale_vista",
"required_fields": []
},
{
"instrument": "tarjeta",
"required_fields": []
},
{
"instrument": "tarjeta_debito",
"required_fields": []
},
{
"instrument": "tarjeta_credito",
"required_fields": []
},
{
"instrument": "bono",
"required_fields": [
"document_number"
]
},
{
"instrument": "otro",
"required_fields": [
"note"
]
},
{
"instrument": "mercadopago",
"required_fields": []
},
{
"instrument": "webpay",
"required_fields": []
},
{
"instrument": "fintoc",
"required_fields": []
},
{
"instrument": "flow",
"required_fields": []
},
{
"instrument": "khipu",
"required_fields": []
},
{
"instrument": "bancame",
"required_fields": []
}
],
"meta": {
"total": 15
}
}Responde: 200 · 400 · 401 · 403 · 404 · 409 · 429
Ajustes de cobro
Cómo cobra el negocio según el tipo de cargo, desde qué monto un link de pago ofrece cuotas, qué cobra «aceptar y pagar» en un presupuesto y la conexión con Mercado Pago.
Tokens personales
La credencial que un miembro emite para sí mismo: ve exactamente lo mismo que esa persona, y deja de servir cuando esa persona deja de tener acceso.