VitrinaAPI

Cobros y pagos

El ledger del workspace: lo que se debe, lo que llegó y cómo se imputa lo segundo a lo primero.

Beta
Puede cambiar en cualquier momento, con una entrada en el changelog y aviso a quienes la llamaron recientemente — ver versionado.

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étodoRutaQué hace
GET/contacts/{id}/ledgerEverything one person owes, has paid, and has in their favour
GET/installment-plansList installment plans
POST/installment-plansCreate an installment plan
GET/installment-plans/{id}Fetch one installment plan
PATCH/installment-plans/{id}Rename an installment plan
POST/installment-plans/{id}/cancelCancel an installment plan
POST/installment-plans/importImport cuotas in bulk (JSON)
POST/installment-plans/import-fileImport cuotas from a CSV file
GET/installmentsList cuotas
GET/installments/{id}Fetch one cuota
PATCH/installments/{id}Change a cuota
POST/installments/{id}/mark-paidMark a cuota paid
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}/checkoutA Mercado Pago payment link for what one cobro still owes
POST/obligations/{id}/waiveForgive what is owed, with a reason
GET/obligations/groups/{groupId}A presupuesto, pack or plan and every line in it
POST/obligations/groups/{groupId}/checkoutA Mercado Pago payment link for a presupuesto, pack or plan
POST/payment-linksSend a customer a Mercado Pago link for one debt
GET/payment-links/optionsWhat a customer owes, and whether a payment link can be sent
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/payments/instrument-fieldsWhich 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
todayquerystringnopatró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ámetroEnTipoObligatorioRestricciones
contact_idqueryuuidno
external_refquerystringnomáx. 120
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 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

CampoTipoObligatorioRestricciones
contact_iduuid—
namestring—máx. 200
phonestring—máx. 40
emailstring—máx. 200
rutstring—máx. 20
labelstring—mín. 1, máx. 280
external_refstring—mín. 1, máx. 120
installmentsobject[]—
scheduleobject—
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
labelstringsí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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsí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

CampoTipoObligatorioRestricciones
rowsobject[]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

CampoTipoObligatorioRestricciones
filenamestringsímín. 1, máx. 255
contentstringsímín. 1, máx. 35000000
content_encodingutf8 \base64—
content_typestring—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ámetroEnTipoObligatorioRestricciones
contact_idqueryuuidno
plan_idqueryuuidno
statusquerypending \paid \overdue \
limitqueryintegerno≥ 1, ≤ 200, por defecto 50
offsetqueryinteger | nullno≥ 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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
amountinteger—≥ 1, ≤ 999999999999
due_datestring—patrón ^\d{4}-\d{2}-\d{2}$
payment_link_urlstring | null—uri, máx. 2000, patrón ^https?:\/\/
reasonstring—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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
paid_onstring—patrón ^\d{4}-\d{2}-\d{2}$
instrumentefectivo \transferencia \cheque \
notestring—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ámetroEnTipoObligatorioRestricciones
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"

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

CampoTipoObligatorioRestricciones
contact_iduuidsí
kindappointment_deposit \service \pack \
expected_clpintegersí≥ 1, ≤ 999999999999
labelstringsímí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"
  }'

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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
todayquerystringnopatró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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
expected_clpinteger—≥ 1, ≤ 999999999999
due_onstring | null—patrón ^\d{4}-\d{2}-\d{2}$
labelstring—mín. 1, máx. 280
reasonstringsí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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsímí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"
  }'

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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsí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ámetroEnTipoObligatorioRestricciones
groupIdpathuuidsí
todayquerystringnopatró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ámetroEnTipoObligatorioRestricciones
groupIdpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
amount_clpinteger—> 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

CampoTipoObligatorioRestricciones
obligation_iduuid—
group_iduuid—
amount_clpinteger—> 0
conversation_iduuid—
deliverboolean—
acknowledgestring[]—
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

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ámetroEnTipoObligatorioRestricciones
contact_idqueryuuidsí
conversation_idqueryuuidno
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 &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.

ParámetroEnTipoObligatorioRestricciones
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"

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

CampoTipoObligatorioRestricciones
contact_idstring | null—
sourcemanual \card_terminal \credit_note
instrumentefectivo \transferencia \cheque \
amount_clpintegersí≥ 1, ≤ 999999999999
paid_onstringsípatró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
saleobject | null—
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
      }
    ]
  }'

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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
allocationsobject[]sí
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
      }
    ]
  }'

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ámetroEnTipoObligatorioRestricciones
idpathuuidsí
allocationIdpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsí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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
contact_idstring | nullsí
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ámetroEnTipoObligatorioRestricciones
idpathuuidsí

Cuerpo

CampoTipoObligatorioRestricciones
reasonstringsí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

En esta página