Factuarea API
Core concepts

Recording payments

Register partial payments against invoices and purchase invoices, and read the running balance from the ledger.

Invoices and purchase invoices keep a payment ledger: a list of individual payments, each with its own amount, date and method. Register payments one at a time as the money comes in — the API recomputes the paid and pending amounts after every entry and flips the document to paid once the balance reaches zero.

There is no separate "partially paid" status. The progress of collection is read from two derived, display-only fields on the invoice: paid_amount (sum of the ledger) and pending_amount (total − paid_amount). A document with pending_amount > 0 is still pending; the one whose pending_amount hits 0 becomes paid.

Register a sale payment

POST /v1/invoices/{id}/payments adds one payment to a sales invoice. The body is small:

FieldTypeRequiredNotes
amountnumberYesGreater than 0. Cannot exceed pending_amount.
paid_onstring (YYYY-MM-DD)YesThe date the money was received.
payment_methodstring (enum)YesOne of the catalog values (see below).
referencestringNoYour own reference (e.g. a transfer number).
notesstringNoFree internal note.

payment_method is a closed enum of seven values: bank_transfer, direct_debit, cash, credit_card, check, paypal, other. Fetch the labelled catalog from GET /v1/payment-methods instead of hardcoding them.

The response is 201 Created with the freshly created payment under 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,
    "created_at": "2026-05-20T10:30:00Z",
    "updated_at": "2026-05-20T10:30:00Z"
  }
}
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'

Partial payments & balance

The running balance does not live on the payment object — it lives on the invoice. After registering one or more payments, read the invoice (GET /v1/invoices/{id}) to see where it stands:

{
  "data": {
    "id": "01931b3e-7c4a-7f2e-9a8b-3c5d6e7f8a01",
    "object": "invoice",
    "status": "pending",
    "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,
          "created_at": "2026-05-20T10:30:00Z",
          "updated_at": "2026-05-20T10:30:00Z"
        }
      ],
      "total": 500.00,
      "pending": 710.00
    }
  }
}
  • paid_amount / pending_amount — the collected and outstanding totals. Always present, computed from the ledger.
  • payments.total / payments.pending — the same two figures, mirrored inside the payments object. Always present.
  • payments.detail — the array of individual payments. Materialised only on the show endpoint (GET /v1/invoices/{id}); in list endpoints it comes back as [] (while total and pending stay populated) to keep listings cheap. Use the sub-resource for the detail on its own.

Once the last payment closes the balance (pending_amount reaches 0), the invoice transitions to paid.

A payment whose amount is greater than pending_amount is rejected with 422 and subcode: "payment_exceeds_pending_amount" (param: "amount"). A payment exactly equal to the pending amount is valid and settles the invoice. See Errors.

List payments

GET /v1/invoices/{id}/payments returns the full ledger of one invoice, newest first. An invoice with no payments returns { "data": [] } — never a 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,
      "created_at": "2026-05-20T10:30:00Z",
      "updated_at": "2026-05-20T10:30:00Z"
    }
  ]
}

Purchase invoice payments

Purchase invoices keep their own ledger (total_retention for IRPF withholding lives on the purchase invoice resource). The contract is asymmetric to the sales side — read it carefully before reusing code:

  • POST /v1/purchase_invoices/{id}/payments returns 201 with the created payment under data (object purchase_invoice_payment), not the full invoice.
  • GET /v1/purchase_invoices/{id}/payments returns { "data": [...] }, newest first.
  • The body adds an optional bank_account_id (integer), and payment_method here is a free string (max 30 chars), not the closed enum used on the sales side.
FieldTypeRequiredNotes
amountnumberYesGreater than 0. Cannot exceed the pending amount.
paid_onstring (YYYY-MM-DD)YesBetween the issue date and today.
payment_methodstringYesFree text, max 30 chars.
bank_account_idintegerNoBank account the payment was made from.
referencestringNoYour own reference.
notesstringNoFree internal note.
{
  "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'

Purchase-invoice payment rules (BR-PUR-019) are enforced as 422: an amount above the pending balance (subcode: "payment_exceeds_pending_amount"), a date outside issue_date … today (subcode: "invalid_payment_date"), or a payment on a cancelled invoice (subcode: "purchase_invoice_not_payable").

Payment methods

GET /v1/payment-methods returns the closed catalog backing the sales payment_method field, each with a value and a human label (Spanish). It is a global enum catalog — not tenant-specific.

{
  "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" }
  ]
}

Read it once at startup and present the labels in your UI; send the value back in payment_method.

Errors

  • 422 payment_exceeds_pending_amount — the amount is larger than the outstanding balance (param: "amount"). This is a business-rule violation, so it is 422, never 409.
  • 409 on a payment POST is reserved for the standard idempotency / conflict envelope (a reused Idempotency-Key with a different body, or a concurrency conflict) — not for the payment data itself.

See Errors for the full envelope and code catalog.

On this page