Cuentas por cobrar
El cuaderno de fiados por API — crear clientes que deben, registrar abonos sin duplicarlos y saldar cuentas.
Un negocio de barrio fía: «Gloria se lleva $300.000 de mercado y me va abonando». Casi siempre eso vive en un cuaderno. Cuentas por cobrar es ese cuaderno dentro de Verifika: una cuenta por cliente, con su saldo, y un historial de movimientos — fiados, abonos y condonaciones. El saldo se calcula solo, y la dueña lo ve en el panel como un tablero por etapas.
La API es para que ese cuaderno se llene solo desde donde ya ocurren las cosas:
| Si tienes… | La usas para… |
|---|---|
| Un POS o una caja | Anotar el fiado cuando el cliente se lleva algo sin pagar, y el abono cuando vuelve. |
| Un ERP o software contable | Sincronizar la cartera: leer saldos, vencidos y el historial para cuadrar el mes. |
| Un bot de cobranza | Sacar la lista de vencidos, escribirles por WhatsApp y registrar el abono cuando pagan. |
Todo cuelga de /v1/receivables. La referencia completa está en
Cuentas por cobrar.
Permisos: son cuatro, no dos
El módulo es una función del plan (receivables_module). Sin ella, cualquier llamada responde
403 PLAN_FEATURE_REQUIRED. Con ella, la llave necesita además el permiso de cada cosa:
| Permiso | Qué abre |
|---|---|
receivables.read | Leer clientes, saldos, el resumen y el historial. |
receivables.write | Crear y editar clientes, registrar fiados y abonos, saldar pagando, archivar y restaurar. |
receivables.write_off | Condonar: perdonar una parte o todo el saldo. |
receivables.void | Anular movimientos. |
¿Por qué no un simple leer/escribir? Porque la integración típica es una caja, y la llave de
una caja no debería poder perdonar deudas ni hacer desaparecer historia. Si se filtra una
llave con receivables.write, lo peor que deja es un movimiento de más, que una persona anula
desde el panel. Con write_off o void se puede borrar lo que un cliente debe. Dale esos dos
solo a quien los necesite.
No hay un permiso de exportar: leer por API ya es llevarse los datos. El Excel y el PDF son para personas, en el panel.
Tampoco hay alcance por sucursal: el cuaderno es del negocio, y una llave acotada a una
sucursal lo ve entero. branchId al crear un cliente es informativo.
Todo lo que escribes queda firmado como API
Clientes y movimientos creados con una llave llevan source: "API" y createdBy: null: detrás
de una llave no hay una persona. El rastro de quién fue está en la llave, que sí tiene dueño.
El estado de cuenta del panel los muestra como «API».
Las etapas se mueven solas
Cada cliente está en una columna del tablero (stage):
stage | En el panel | Cómo se llega |
|---|---|---|
OPEN | Por cobrar | Cliente nuevo con deuda, o un fiado nuevo sobre una cuenta que ya estaba saldada. |
PARTIAL | Abonando | Solo, con el primer abono mientras quede saldo. |
FOLLOW_UP | En seguimiento | A mano (PATCH { "stage": "FOLLOW_UP" }): «hay que llamarlo». No se mueve hasta que salde. |
PAID | Saldada | Solo, cuando el saldo llega a cero. |
Después de cada movimiento o anulación se aplican estas reglas, en orden:
- Saldo en cero →
PAID. - Saldo mayor que cero y estaba
PAID→ vuelve aOPEN. - Un abono sobre
OPEN→PARTIAL. FOLLOW_UPse respeta hasta que el saldo llegue a cero.- Anular el único abono de una cuenta
PARTIALla devuelve aOPEN.
Dos cosas que no puedes hacer moviendo la tarjeta:
- Pasarla a
PAIDcon saldo. Eso no es mover una tarjeta, es decir que pagó: responde422 RECEIVABLE_STAGE_REQUIRES_PAYMENT. Para darla por pagada, salda la cuenta. - Mover de columna una cuenta en cero. No hay nada que cobrar:
422 RECEIVABLE_ALREADY_PAID. Para reabrirla se anota un fiado nuevo.
«Vencida» no es una etapa
Es un dato aparte, isOverdue: saldo mayor que cero y la fecha acordada de pago
(dueDate) quedó antes de hoy, contando el día en Bogotá. Una cuenta en PARTIAL o en
FOLLOW_UP puede estar vencida. Para la lista de vencidos: GET /v1/receivables?overdue=true.
Montos y fechas
- Pesos con hasta 2 decimales.
300000son $300.000 y25000.2son $25.000,20. Saldos y totales vuelven igual: con centavos solo si los tienen. Un monto con más de 2 decimales es400(«El valor admite máximo 2 decimales.»), no se redondea. amountsiempre positivo. El signo lo da el tipo:CHARGE(fiado) sube el saldo;PAYMENT(abono) yWRITE_OFF(condonación) lo bajan.- Un día pelado es un día de Bogotá. En los filtros,
createdFrom=2026-09-01arranca a las 00:00 de Colombia ycreatedTo=2026-09-30cierra a las 23:59:59 de Colombia — no a medianoche UTC, que serían las 7 de la noche del día anterior. dueDate: "2026-10-15"se guarda a las 00:00 de ese día en Bogotá (2026-10-15T05:00:00.000Z), así que la cuenta se vence el 16.occurredAtde un movimiento es opcional (sin él, ahora). Un día pelado ("2026-09-26") queda al mediodía de Bogotá, para que un abono «del 26» no caiga el 25 por ningún redondeo. Un ISO completo se respeta tal cual; si está en el futuro (con 5 minutos de tolerancia), es400.
El saldo no se suma: se recalcula
Cada escritura bloquea la fila del cliente, anota el movimiento y relee todo el historial
para recalcular saldo, totales y el balanceAfter de cada movimiento. Por eso un abono con fecha
de ayer o la anulación de uno de hace dos semanas dejan el historial cuadrado, y dos abonos
simultáneos —la caja y el panel a la vez— nunca pasan los dos la validación contra el mismo saldo.
El flujo completo
De cliente nuevo a cuenta saldada, en cuatro llamadas.
1. Crear el cliente con su deuda
Gloria se llevó $300.000 y dejó $50.000 de una vez. Con initialDebt e initialPaid la cuenta
nace con los dos movimientos («Deuda inicial» y «Abono inicial») y ya en PARTIAL:
curl -X POST https://api.verifika.tech/v1/receivables \
-H "Authorization: Bearer $VERIFIKA_API_KEY" \
-H "Idempotency-Key: alta-gloria-restrepo-2026-09-22" \
-H "Content-Type: application/json" \
-d '{
"name": "Gloria Restrepo",
"phone": "310 456 7890",
"notes": "Mercado de la semana. Paga los viernes.",
"dueDate": "2026-10-15",
"initialDebt": 300000,
"initialPaid": 50000
}'{
"object": "receivableAccount",
"id": "cmu4a2k9x000b7qw1h3r5tz8e",
"name": "Gloria Restrepo",
"phone": "+573104567890",
"stage": "PARTIAL",
"dueDate": "2026-10-15T05:00:00.000Z",
"isOverdue": false,
"balance": 250000,
"currency": "COP",
"totalCharged": 300000,
"totalPaid": 50000,
"source": "API",
"createdBy": null,
"entries": {
"object": "list",
"data": [
{ "object": "receivableEntry", "type": "PAYMENT", "amount": 50000, "balanceAfter": 250000, "note": "Abono inicial" },
{ "object": "receivableEntry", "type": "CHARGE", "amount": 300000, "balanceAfter": 300000, "note": "Deuda inicial" }
],
"hasMore": false,
"nextCursor": null
}
}El teléfono se normaliza solo: un celular colombiano de 10 dígitos gana el +57. La API no
deduplica por nombre: antes de crear, busca con GET /v1/receivables?q=gloria que no exista
ya — dos «Gloria» en el cuaderno parten el saldo en dos.
2. Registrar un abono
El viernes Gloria abona $80.000 en la caja 2. La Idempotency-Key es lo que evita que un
reintento lo anote dos veces (más abajo):
curl -X POST https://api.verifika.tech/v1/receivables/cmu4a2k9x000b7qw1h3r5tz8e/entries \
-H "Authorization: Bearer $VERIFIKA_API_KEY" \
-H "Idempotency-Key: caja2-recibo-004817" \
-H "Content-Type: application/json" \
-d '{
"type": "PAYMENT",
"amount": 80000,
"note": "Abono en efectivo, caja 2"
}'La respuesta es el movimiento con la cuenta como quedó en account, porque la pregunta que
sigue a «registré un abono» es siempre «¿y cuánto debe ahora?»:
{
"object": "receivableEntry",
"id": "cmu4h7r2t0014bnw1y2e6kd9v",
"type": "PAYMENT",
"amount": 80000,
"balanceAfter": 170000,
"note": "Abono en efectivo, caja 2",
"source": "API",
"voided": false,
"account": {
"object": "receivableAccount",
"id": "cmu4a2k9x000b7qw1h3r5tz8e",
"stage": "PARTIAL",
"balance": 170000,
"totalPaid": 130000,
"paymentsCount": 2
}
}Un fiado nuevo es la misma llamada con "type": "CHARGE".
3. Consultar el saldo
curl https://api.verifika.tech/v1/receivables/cmu4a2k9x000b7qw1h3r5tz8e \
-H "Authorization: Bearer $VERIFIKA_API_KEY"Devuelve la ficha con balance, isOverdue, los totales y los últimos 50 movimientos,
recientes primero y con los anulados marcados voided: true. Si entries.hasMore es true,
el resto del historial está en GET /v1/receivables/{id}/entries, paginado por cursor.
Para el cuaderno entero, GET /v1/receivables/summary da el total que le deben al negocio, lo
vencido, el reparto por etapa y los cinco que más deben — sumado en la base. No sumes páginas
de la lista para esto: está paginada.
4. Saldar la cuenta
Gloria paga todo lo que queda. En vez de mandar un abono por $170.000, pide que se salde:
curl -X POST https://api.verifika.tech/v1/receivables/cmu4a2k9x000b7qw1h3r5tz8e/settle \
-H "Authorization: Bearer $VERIFIKA_API_KEY" \
-H "Idempotency-Key: caja2-recibo-004902" \
-H "Content-Type: application/json" \
-d '{ "mode": "PAYMENT" }'El monto no se manda: el servidor lee el saldo con la fila bloqueada y registra un abono
por exactamente eso. Si entre tu consulta y este POST entró otro abono desde el panel, se salda
lo que de verdad queda, no lo que tú creías. La cuenta pasa a PAID y sale el evento
receivable.account.paid.
{
"object": "receivableEntry",
"type": "PAYMENT",
"amount": 170000,
"balanceAfter": 0,
"note": "Pagó el saldo",
"account": { "id": "cmu4a2k9x000b7qw1h3r5tz8e", "stage": "PAID", "balance": 0, "totalPaid": 300000 }
}Con "mode": "WRITE_OFF" el saldo se perdona en vez de cobrarse, y pide receivables.write_off.
Reintentar un abono sin duplicarlo
La red se corta después de que registramos el abono pero antes de que te llegue la respuesta. Tu caja reintenta. Sin protección, Gloria queda con dos abonos de $80.000 — y el saldo del cuaderno, $80.000 más bajo de lo que de verdad debe.
Por eso toda escritura acepta Idempotency-Key. La regla es una llave por operación de
negocio, no por intento HTTP: el número de recibo de la caja es una llave perfecta, porque ya
identifica ese abono y solo ese.
| Reintentas con… | Qué pasa |
|---|---|
| La misma llave y el mismo cuerpo | Te devolvemos la respuesta original, con Idempotent-Replay: true. No se anota otro abono. |
| La misma llave mientras el primero sigue en curso | 409 IDEMPOTENCY_IN_PROGRESS: espera unos segundos y reintenta. |
| La misma llave con otro cuerpo | 409 IDEMPOTENCY_KEY_REUSED: es otra operación, usa otra llave. |
Solo se guardan las respuestas exitosas: si el abono falló con 422, corriges y reintentas con
la misma llave. Las llaves se recuerdan 24 horas. Detalles en Idempotencia.
Errores que vas a ver
Todos con el sobre de siempre ({ error: { code, message, details?, requestId, docUrl } },
ver Errores). Ramifica por code, nunca por message.
| HTTP | code | Cuándo | Qué hacer |
|---|---|---|---|
| 422 | RECEIVABLE_OVERPAYMENT | Un abono o una condonación por más de lo que debe. details.balance es el saldo real. | No lo ajustes por tu cuenta: muéstrale a la persona el saldo y deja que decida si registra ese valor. Si pagó todo, usa /settle. |
| 422 | RECEIVABLE_ALREADY_PAID | Saldar una cuenta que ya está en cero, o moverla de etapa. | Nada que cobrar. Si se llevó algo nuevo, es un fiado (CHARGE). |
| 422 | RECEIVABLE_STAGE_REQUIRES_PAYMENT | PATCH { "stage": "PAID" } con saldo. details.balance dice cuánto debe. | Usa POST …/settle. |
| 422 | RECEIVABLE_NEGATIVE_BALANCE | Anular un fiado que ya se había abonado: el cliente quedaría con saldo a favor. | Anula primero el abono, después el fiado. |
| 403 | SCOPE_REQUIRED | La llave no tiene el permiso. details.scope dice cuál. | Ver abajo. |
| 404 | NOT_FOUND | El cliente o el movimiento no existen, no son de tu negocio, o el cliente está archivado y le intentas escribir. | Si está archivado, restáuralo primero. |
| 400 | BAD_REQUEST | Monto con más de 2 decimales o en cero, teléfono que no cuadra, initialPaid mayor que initialDebt, occurredAt en el futuro. | Corrige el cuerpo. details trae el campo. |
{
"error": {
"code": "RECEIVABLE_OVERPAYMENT",
"message": "El monto supera lo que debe ($170.000).",
"details": { "balance": 170000 },
"requestId": "req_d20f6e…",
"docUrl": "https://docs.verifika.tech/guias/errores"
}
}Condonar con la llave de la caja
La caja intenta perdonar el saldo ("type": "WRITE_OFF" o settle con "mode": "WRITE_OFF")
con una llave que solo tiene receivables.write:
{
"error": {
"code": "SCOPE_REQUIRED",
"message": "Esta API key no tiene permiso para esta operación.",
"details": { "scope": "receivables.write_off" },
"requestId": "req_b0b926…",
"docUrl": "https://docs.verifika.tech/guias/errores"
}
}Es lo esperado, no un fallo: condonar es una decisión de la dueña, no de la caja. No amplíes
la llave de la caja para esquivarlo; que lo haga una persona desde el panel, o una llave aparte
con ese permiso. Como el permiso depende del type o del mode, se revisa después de
validar el cuerpo: un cuerpo inválido responde 400 antes que 403.
Anular, archivar o condonar
Las tres «quitan» algo, y no son intercambiables:
| Anular | Archivar | Condonar | |
|---|---|---|---|
| Para qué | Un movimiento mal registrado (abono duplicado, monto errado). | Un cliente al que ya no se le cobra (se mudó, cerró). | Una deuda que el negocio decide perdonar. |
| Llamada | POST …/entries/{entryId}/void | POST …/archive · POST …/restore | POST …/entries con WRITE_OFF, o POST …/settle con mode: WRITE_OFF |
| Permiso | receivables.void | receivables.write | receivables.write_off |
| El saldo | Se recalcula sin ese movimiento. | No cambia: sigue ahí, solo sale del tablero y de los totales. | Baja. Queda en totalWrittenOff, no en lo abonado. |
| El historial | El movimiento sigue, con voided: true. Nada se borra. | Intacto y legible. Mientras esté archivado no admite movimientos ni ediciones. | Un movimiento más, de tipo WRITE_OFF. |
| ¿Se deshace? | No se «des-anula»: se registra de nuevo. | Sí, con restore, tal como estaba. | Anulando la condonación. |
No hay forma de borrar un cliente ni un movimiento por API. Es a propósito: el cuaderno es la memoria de lo que se fió, y un historial que se puede reescribir deja de servir para cobrar.
Enterarte sin preguntar
Si tu sistema necesita saber cuándo alguien —la cajera en la app, la dueña en el panel, el
asistente o tu propia llave— anota un fiado o salda una cuenta, suscríbete a los eventos
receivable.* en vez de consultar cada minuto. Están en
Eventos de webhook. El que casi todos
quieren es receivable.account.paid.
Siguiente paso
Referencia de Cuentas por cobrar
Los once endpoints, con filtros, cuerpos, respuestas y playground.
Eventos de cuentas por cobrar
Qué te llega cuando se fía, se abona o se salda.
Desde un asistente
«¿Quién me debe más?» y «Gloria abonó 50 mil», por MCP.
Paginación
Recorrer todo el cuaderno sin repetir ni saltarte clientes.