Registrar pagos
Registra pagos parciales, consulta el saldo en curso desde el ledger y anula un pago que se ha devuelto — sin emitir una rectificativa que nadie te ha pedido.
Las facturas y las facturas de compra mantienen un ledger de pagos:
una lista de pagos individuales, cada uno con su propio importe, fecha y
método. Registra los pagos de uno en uno a medida que entra el dinero — la
API recalcula los importes cobrado y pendiente después de cada
entrada, y la factura se reporta como paid cuando el saldo llega a cero.
El ledger es de solo adición. El dinero que entró y volvió a salir (una devolución de adeudo, una retrocesión de tarjeta, un pago imputado a la factura equivocada) no se borra: la entrada se anula, conserva su importe, fecha, método y referencia, y gana un motivo, un instante y un autor. Un pago anulado deja de computar, así que la factura vuelve al circuito de cobro.
El status que lees en la factura se deriva del ledger, no se
almacena: paid cuando los pagos vigentes cubren el importe exigible,
partially_paid mientras cubren una parte, y en cualquier otro caso el
estado propio del documento (sent, u overdue en cuanto pasa su
vencimiento). Registrar o anular un pago lo cambia en la lectura siguiente
— no hay ningún paso de sincronización que pueda quedarse atrás.
Los dos importes que hay detrás son paid_amount (suma de los pagos
vigentes) y pending_amount (importe exigible − paid_amount).
El ciclo completo de un pago de venta es, por tanto:
POST /v1/invoices/{id}/payments.paid_amount / pending_amount, o el sub-recurso del ledger.POST /v1/invoices/{id}/payments/{payment_id}/reversal. La factura vuelve a sent o a overdue y admite un pago nuevo.Registrar un pago de venta
POST /v1/invoices/{id}/payments añade un pago a una factura de venta.
El body es pequeño:
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
amount | number | Sí | Mayor que 0. No puede superar pending_amount. |
paid_on | string (YYYY-MM-DD) | Sí | La fecha en que se recibió el dinero. |
payment_method | string (enum) | Sí | Uno de los valores del catálogo (ver abajo). |
reference | string | No | Tu propia referencia (p. ej. un número de transferencia). |
notes | string | No | Nota interna libre. |
payment_method es un enum cerrado de siete valores: bank_transfer,
direct_debit, cash, credit_card, check, paypal, other. Obtén el
catálogo con etiquetas desde GET /v1/payment-methods
en lugar de fijar los valores a mano.
La respuesta es 201 Created con el pago recién creado bajo data:
{
"data": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b",
"object": "payment",
"invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"amount": 500.00,
"payment_date": "2026-05-20",
"payment_method": "bank_transfer",
"payment_method_text": "Transferencia bancaria",
"reference": "TRF-2026-0042",
"notes": null,
"is_reversed": false,
"reversed_at": null,
"reversal_reason": null,
"reversal_reason_text": null,
"reversal_note": null,
"created_at": "2026-05-20T10:30:00Z",
"updated_at": "2026-05-20T10:30:00Z"
}
}Los cinco campos revers* describen el estado de esa entrada en el
ledger. Un pago vigente informa is_reversed: false y null en los
otros cuatro; ver Anular un pago.
import os, requests
resp = requests.post(
'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments',
json={
'amount': 500.00,
'paid_on': '2026-05-20',
'payment_method': 'bank_transfer',
'reference': 'TRF-2026-0042',
},
headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"},
)
resp.raise_for_status()
payment = resp.json()['data']
print(payment['id'], payment['amount'])const res = await fetch(
'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 500.0,
paid_on: '2026-05-20',
payment_method: 'bank_transfer',
reference: 'TRF-2026-0042',
}),
},
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
console.log(data.id, data.amount);curl -s -X POST \
https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 500.00,
"paid_on": "2026-05-20",
"payment_method": "bank_transfer",
"reference": "TRF-2026-0042"
}' | jq '.data'Pagos parciales y saldo
El saldo en curso no vive en el objeto del pago — vive en la
factura. Tras registrar uno o varios pagos, lee la factura
(GET /v1/invoices/{id}) para ver cómo está:
{
"data": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"object": "invoice",
"status": "sent",
"total": 1210.00,
"paid_amount": 500.00,
"pending_amount": 710.00,
"payments": {
"detail": [
{
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b",
"object": "payment",
"invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"amount": 500.00,
"payment_date": "2026-05-20",
"payment_method": "bank_transfer",
"payment_method_text": "Transferencia bancaria",
"reference": "TRF-2026-0042",
"notes": null,
"is_reversed": false,
"reversed_at": null,
"reversal_reason": null,
"reversal_reason_text": null,
"reversal_note": null,
"created_at": "2026-05-20T10:30:00Z",
"updated_at": "2026-05-20T10:30:00Z"
}
],
"total": 500.00,
"pending": 710.00
}
}
}paid_amount/pending_amount— los totales cobrado y pendiente. Siempre presentes, calculados a partir de los pagos vigentes: una entrada anulada sigue enpayments.detail, pero ya no suma apaid_amount.payments.total/payments.pending— las mismas dos cifras, reflejadas dentro del objetopayments. Siempre presentes.payments.detail— el array de pagos individuales. Se materializa solo en el endpoint de detalle (GET /v1/invoices/{id}); en los endpoints de listado llega como[](mientrastotalypendingsiguen poblados) para que los listados sean ligeros. Usa el sub-recurso para obtener el detalle por separado.
Cuando el último pago cierra el saldo (pending_amount llega a 0), la
factura reporta status: "paid".
Un pago cuyo amount supera pending_amount se rechaza con 422 y
subcode: "payment_exceeds_pending_amount" (param: "amount"). Un pago
exactamente igual al importe pendiente es válido y salda la factura.
Consulta Errores.
Listar pagos
GET /v1/invoices/{id}/payments devuelve el ledger completo de una
factura, ordenado por fecha de pago. Una factura sin pagos devuelve
{ "data": [] }, nunca un 404.
{
"data": [
{
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b",
"object": "payment",
"invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"amount": 500.00,
"payment_date": "2026-05-20",
"payment_method": "bank_transfer",
"payment_method_text": "Transferencia bancaria",
"reference": "TRF-2026-0042",
"notes": null,
"is_reversed": false,
"reversed_at": null,
"reversal_reason": null,
"reversal_reason_text": null,
"reversal_note": null,
"created_at": "2026-05-20T10:30:00Z",
"updated_at": "2026-05-20T10:30:00Z"
}
]
}Los pagos anulados siguen en esta lista. Su ausencia nunca es la
señal: la señal es is_reversed: true. El código que detecte una
anulación porque una entrada desaparece del ledger no se disparará
jamás, porque no se borra nada. Filtra por el flag y, si recalculas el
saldo por tu cuenta, suma solo las entradas con is_reversed: false.
Anular un pago
POST /v1/invoices/{id}/payments/{payment_id}/reversal anula un pago de
una factura de venta indicando por qué. El pago no se borra: un pago
que existió y dejó de tener efecto es información contable, y su rastro
(motivo, instante y autor) es lo que explica por qué la factura dejó de
estar cobrada.
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
reason | string (enum) | Sí | Uno de los cinco valores del catálogo cerrado de abajo. |
note | string | No | Nota libre, máx. 500 caracteres. |
La respuesta es 200 OK con el pago ya anulado bajo data — la
misma forma InvoicePaymentDetail que devuelven el alta y el listado:
{
"data": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b",
"object": "payment",
"invoice_id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
"amount": 500.00,
"payment_date": "2026-05-20",
"payment_method": "direct_debit",
"payment_method_text": "Domiciliación bancaria",
"reference": "TRF-2026-0042",
"notes": null,
"is_reversed": true,
"reversed_at": "2026-06-02T08:15:00Z",
"reversal_reason": "direct_debit_return",
"reversal_reason_text": "Devolución de adeudo SEPA",
"reversal_note": "Devuelto por el banco con motivo MD01.",
"created_at": "2026-05-20T10:30:00Z",
"updated_at": "2026-06-02T08:15:00Z"
}
}El catálogo de motivos es cerrado
reason es obligatorio y solo se admiten estos cinco valores. No existe
un valor refund a propósito: un reembolso genuino es una factura
rectificativa, no una anulación (ver
¿Anulación o rectificativa?).
reason | Cuándo usarlo |
|---|---|
direct_debit_return | El banco del cliente devolvió el adeudo SEPA. |
card_dispute | El pago con tarjeta se retrocedió tras una disputa. |
misapplied_payment | El dinero entró, pero se imputó a la factura equivocada. |
bounced_effect | Un efecto o pagaré resultó impagado al vencimiento. |
recording_error | La entrada fue un error: nunca correspondió a dinero real. |
Cualquier otro valor devuelve 422 payment_reversal_reason_invalid con
param: "reason".
Qué le pasa a la factura
El importe anulado deja de computar en paid_amount, pending_amount y
en cualquier agregado de tesorería, así que una factura paid vuelve
al circuito de cobro en la lectura siguiente:
overduesi su fecha de vencimiento ya pasó,senten caso contrario,- y vuelve a admitir un pago nuevo.
No hay forma de «despagar» una factura a mano: paid es una lectura del
ledger, no un estado que se pueda escribir. El endpoint genérico de cambio
de estado no lo admite como destino, precisamente para que el saldo cobrado
y el ledger nunca puedan discrepar. Anular un pago es el único camino de
vuelta.
import os, requests
resp = requests.post(
'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01'
'/payments/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/reversal',
json={
'reason': 'direct_debit_return',
'note': 'Devuelto por el banco con motivo MD01.',
},
headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"},
)
resp.raise_for_status()
payment = resp.json()['data']
print(payment['is_reversed'], payment['reversal_reason'])const res = await fetch(
'https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01' +
'/payments/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/reversal',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
reason: 'direct_debit_return',
note: 'Devuelto por el banco con motivo MD01.',
}),
},
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
console.log(data.is_reversed, data.reversal_reason);curl -s -X POST \
https://api.factuarea.com/v1/invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01/payments/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0b/reversal \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "direct_debit_return",
"note": "Devuelto por el banco con motivo MD01."
}' | jq '.data | {is_reversed, reversal_reason, reversal_reason_text}'Irreversible. No hay des-anulación: anular por segunda vez el mismo
pago devuelve 422 payment_already_reversed, porque pisar el primer
rastro borraría el registro de por qué la factura dejó de estar
cobrada. Si el dinero volvió a entrar, registra un pago nuevo.
La ruta está anidada a propósito: en la API v1 un pago de venta solo
existe colgando de su factura. Cruzar una factura y un pago que no se
corresponden devuelve 404 —no 422—, igual que un pago de otra
empresa: los tres casos colapsan en la misma respuesta sin revelar cuál
de ellos era.
Errores de la anulación
| Estado | Código | Significado |
|---|---|---|
422 | payment_reversal_reason_invalid | reason está fuera del catálogo cerrado (param: "reason"). |
422 | payment_reversal_invalid | note no es válida — p. ej. supera los 500 caracteres (param: "note"). |
422 | payment_already_reversed | El pago ya estaba anulado. |
404 | resource_not_found | Factura o pago desconocidos, par que no se corresponde, o datos de otra empresa. |
La anulación necesita el scope invoices:write, el mismo que registra un
pago. No existe una familia de scopes payments:*: en la API pública un
pago de venta es parte de su factura, y la credencial que puede cobrar
una factura es la que puede deshacer ese cobro.
¿Anulación o rectificativa?
Esta es la decisión que importa, y equivocarse tiene consecuencias fiscales. Anular un pago no es emitir una factura rectificativa.
- El dinero volvió, pero la operación no se minora — una devolución
de adeudo, una retrocesión de tarjeta, un efecto impagado, un pago
imputado a la factura equivocada. El cliente tiene su dinero de vuelta,
pero tú vendiste lo que vendiste y te lo siguen debiendo. Anula el
pago: la deuda sigue viva, la factura vuelve a
sento aoverduey reaparece en tu pendiente de cobro. Los ingresos no cambian, así que no hay nada que rectificar. - Lo que se minora es la operación — un reembolso genuino, un
descuento concedido a posteriori, una devolución de mercancía, un
importe mal facturado. Ahí los ingresos sí bajan, y eso es lo que
documenta una factura rectificativa
(
POST /v1/invoices/{id}/corrective). La factura original conserva su cobro; la rectificativa es el documento que minora la base imponible.
| Qué ha pasado | Anular el pago | Factura rectificativa |
|---|---|---|
| Devolución de adeudo SEPA | direct_debit_return | — |
| Retrocesión o disputa de tarjeta | card_dispute | — |
| Efecto impagado | bounced_effect | — |
| Pago imputado a la factura equivocada | misapplied_payment | — |
| Entrada que nunca correspondió a dinero real | recording_error | — |
| Reembolso genuino al cliente | — | Sí |
| Descuento posterior, devolución de mercancía | — | Sí |
| Importe, impuesto o cliente equivocados | — | Sí |
Una anulación nunca emite una rectificativa, y nunca toca el registro VeriFactu: la factura que enviaste sigue siendo la factura que enviaste. Lo único que cambió fue su cobro.
Un recibo devuelto no es un crédito incobrable. Anular un pago no minora tu IVA repercutido y no es la vía del art. 80.Cuatro LIVA, que tiene sus propios requisitos formales — reclamación judicial o requerimiento notarial, sus propios plazos y una comunicación a la Administración. Una integración que reaccione a una devolución emitiendo una rectificativa declarará una minoración de ingresos que no ha ocurrido.
El evento payment.reversed
Cada anulación emite payment.reversed, para que una integración se
entere de que un cobro se ha deshecho sin tener que ir a preguntarlo. El
payload lleva el pago anulado bajo data.object más un bloque
data.reversal con el reason y el origin: gateway cuando fue la
pasarela de pago la que informó de la devolución, disputa o retrocesión
(ya hubo movimiento real en el banco), y manual cuando lo registró una
persona.
Suscríbete a él allí donde ya reaccionas a payment.received: una
factura que diste por cobrada puede dejar de estarlo y, hasta que este
evento existió, no había forma de enterarse. Ver
Eventos para el payload y
Webhooks para la entrega y la firma.
Pagos de factura de compra
Las facturas de compra mantienen su propio ledger (total_retention, la
retención IRPF agregada, vive en el recurso de la factura de compra). El
contrato es asimétrico respecto al de venta — léelo con atención antes
de reutilizar código:
POST /v1/purchase_invoices/{id}/paymentsdevuelve201con el pago creado bajodata(objetopurchase_invoice_payment), no la factura completa.GET /v1/purchase_invoices/{id}/paymentsdevuelve{ "data": [...] }, del más reciente al más antiguo.- No hay endpoint de anulación en el lado de compra: anular es una operación de la factura de venta, porque lo que restituye es una deuda a tu favor.
- El body añade un
bank_account_idopcional (entero), y aquípayment_methodes un string libre (máx. 30 caracteres), no el enum cerrado que se usa en el lado de venta.
| Campo | Tipo | Requerido | Notas |
|---|---|---|---|
amount | number | Sí | Mayor que 0. No puede superar el importe pendiente. |
paid_on | string (YYYY-MM-DD) | Sí | Entre la fecha de emisión y hoy. |
payment_method | string | Sí | Texto libre, máx. 30 caracteres. |
bank_account_id | integer | No | Cuenta bancaria desde la que se hizo el pago. |
reference | string | No | Tu propia referencia. |
notes | string | No | Nota interna libre. |
{
"data": {
"id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a0c",
"object": "purchase_invoice_payment",
"amount": 423.50,
"paid_on": "2026-05-21",
"payment_method": "transferencia",
"bank_account_id": 12,
"reference": "TRF-2026-0099",
"notes": null,
"created_at": "2026-05-21T09:00:00Z"
}
}import os, requests
resp = requests.post(
'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments',
json={
'amount': 423.50,
'paid_on': '2026-05-21',
'payment_method': 'transferencia',
'bank_account_id': 12,
},
headers={'Authorization': f"Bearer {os.environ['FACTUAREA_API_KEY']}"},
)
resp.raise_for_status()
print(resp.json()['data']['id'])const res = await fetch(
'https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FACTUAREA_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 423.5,
paid_on: '2026-05-21',
payment_method: 'transferencia',
bank_account_id: 12,
}),
},
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
console.log(data.id);curl -s -X POST \
https://api.factuarea.com/v1/purchase_invoices/01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a05/payments \
-H "Authorization: Bearer $FACTUAREA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 423.50,
"paid_on": "2026-05-21",
"payment_method": "transferencia",
"bank_account_id": 12
}' | jq '.data'Las reglas de pago de la factura de compra (BR-PUR-019) se aplican como
422: un importe por encima del saldo pendiente
(subcode: "payment_exceeds_pending_amount"), una fecha fuera de
fecha_emisión … hoy (subcode: "invalid_payment_date"), o un pago sobre
una factura cancelada (subcode: "purchase_invoice_not_payable").
Métodos de pago
GET /v1/payment-methods devuelve el catálogo cerrado que respalda el
campo payment_method de venta, cada uno con un value y una etiqueta
legible (en español). Es un catálogo de enum global — no específico de
empresa.
{
"data": [
{ "value": "bank_transfer", "label": "Transferencia bancaria" },
{ "value": "direct_debit", "label": "Domiciliación bancaria" },
{ "value": "cash", "label": "Efectivo" },
{ "value": "credit_card", "label": "Tarjeta de crédito" },
{ "value": "check", "label": "Cheque" },
{ "value": "paypal", "label": "PayPal" },
{ "value": "other", "label": "Otro" }
]
}Léelo una vez al arrancar y muestra las etiquetas en tu interfaz; devuelve
el value en payment_method.
Errores
422payment_exceeds_pending_amount— el importe es mayor que el saldo pendiente (param: "amount"). Es una violación de regla de negocio, así que es422, nunca409.422payment_reversal_reason_invalid— elreasonde la anulación está fuera del catálogo cerrado (param: "reason").422payment_reversal_invalid— lanotede la anulación no es válida, p. ej. supera los 500 caracteres (param: "note").422payment_already_reversed— ese pago ya estaba anulado; registra uno nuevo en vez de deshacer la anulación.409en unPOSTde pago se reserva para el envoltorio estándar de idempotencia / conflicto (unIdempotency-Keyreutilizado con un body distinto, o un conflicto de concurrencia) — no para los datos del pago en sí.
Consulta Errores para el envoltorio completo y el catálogo de códigos.