Factuarea APIDevelopers

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:

RegístraloPOST /v1/invoices/{id}/payments.
Lee el saldo en la factura — paid_amount / pending_amount, o el sub-recurso del ledger.
Anúlalo si el dinero se ha devuelto — 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:

CampoTipoRequeridoNotas
amountnumberMayor que 0. No puede superar pending_amount.
paid_onstring (YYYY-MM-DD)La fecha en que se recibió el dinero.
payment_methodstring (enum)Uno de los valores del catálogo (ver abajo).
referencestringNoTu propia referencia (p. ej. un número de transferencia).
notesstringNoNota 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 en payments.detail, pero ya no suma a paid_amount.
  • payments.total / payments.pending — las mismas dos cifras, reflejadas dentro del objeto payments. 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 [] (mientras total y pending siguen 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.

CampoTipoRequeridoNotas
reasonstring (enum)Uno de los cinco valores del catálogo cerrado de abajo.
notestringNoNota 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?).

reasonCuándo usarlo
direct_debit_returnEl banco del cliente devolvió el adeudo SEPA.
card_disputeEl pago con tarjeta se retrocedió tras una disputa.
misapplied_paymentEl dinero entró, pero se imputó a la factura equivocada.
bounced_effectUn efecto o pagaré resultó impagado al vencimiento.
recording_errorLa 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:

  • overdue si su fecha de vencimiento ya pasó,
  • sent en 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

EstadoCódigoSignificado
422payment_reversal_reason_invalidreason está fuera del catálogo cerrado (param: "reason").
422payment_reversal_invalidnote no es válida — p. ej. supera los 500 caracteres (param: "note").
422payment_already_reversedEl pago ya estaba anulado.
404resource_not_foundFactura 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 sent o a overdue y 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 pasadoAnular el pagoFactura rectificativa
Devolución de adeudo SEPAdirect_debit_return
Retrocesión o disputa de tarjetacard_dispute
Efecto impagadobounced_effect
Pago imputado a la factura equivocadamisapplied_payment
Entrada que nunca correspondió a dinero realrecording_error
Reembolso genuino al cliente
Descuento posterior, devolución de mercancía
Importe, impuesto o cliente equivocados

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}/payments devuelve 201 con el pago creado bajo data (objeto purchase_invoice_payment), no la factura completa.
  • GET /v1/purchase_invoices/{id}/payments devuelve { "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_id opcional (entero), y aquí payment_method es un string libre (máx. 30 caracteres), no el enum cerrado que se usa en el lado de venta.
CampoTipoRequeridoNotas
amountnumberMayor que 0. No puede superar el importe pendiente.
paid_onstring (YYYY-MM-DD)Entre la fecha de emisión y hoy.
payment_methodstringTexto libre, máx. 30 caracteres.
bank_account_idintegerNoCuenta bancaria desde la que se hizo el pago.
referencestringNoTu propia referencia.
notesstringNoNota 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

  • 422 payment_exceeds_pending_amount — el importe es mayor que el saldo pendiente (param: "amount"). Es una violación de regla de negocio, así que es 422, nunca 409.
  • 422 payment_reversal_reason_invalid — el reason de la anulación está fuera del catálogo cerrado (param: "reason").
  • 422 payment_reversal_invalid — la note de la anulación no es válida, p. ej. supera los 500 caracteres (param: "note").
  • 422 payment_already_reversed — ese pago ya estaba anulado; registra uno nuevo en vez de deshacer la anulación.
  • 409 en un POST de pago se reserva para el envoltorio estándar de idempotencia / conflicto (un Idempotency-Key reutilizado 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.

En esta página

¿Te echamos una mano?Contactar con soporte