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:
| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | Yes | Greater than 0. Cannot exceed pending_amount. |
paid_on | string (YYYY-MM-DD) | Yes | The date the money was received. |
payment_method | string (enum) | Yes | One of the catalog values (see below). |
reference | string | No | Your own reference (e.g. a transfer number). |
notes | string | No | Free 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 thepaymentsobject. 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[](whiletotalandpendingstay 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}/paymentsreturns201with the created payment underdata(objectpurchase_invoice_payment), not the full invoice.GET /v1/purchase_invoices/{id}/paymentsreturns{ "data": [...] }, newest first.- The body adds an optional
bank_account_id(integer), andpayment_methodhere is a free string (max 30 chars), not the closed enum used on the sales side.
| Field | Type | Required | Notes |
|---|---|---|---|
amount | number | Yes | Greater than 0. Cannot exceed the pending amount. |
paid_on | string (YYYY-MM-DD) | Yes | Between the issue date and today. |
payment_method | string | Yes | Free text, max 30 chars. |
bank_account_id | integer | No | Bank account the payment was made from. |
reference | string | No | Your own reference. |
notes | string | No | Free 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
422payment_exceeds_pending_amount— the amount is larger than the outstanding balance (param: "amount"). This is a business-rule violation, so it is422, never409.409on a paymentPOSTis reserved for the standard idempotency / conflict envelope (a reusedIdempotency-Keywith a different body, or a concurrency conflict) — not for the payment data itself.
See Errors for the full envelope and code catalog.