Allocating payments
A payment and the documents it settles are separate things. You record the money once, then apply it — in whole or in parts, now or later. Whatever a payment does not settle is an advance. This page walks a complete integration end to end, with the exact bodies on the wire.
The two ceilings
Every allocation is checked against both of these. The first is the one integrations forget:
| Ceiling | Meaning |
|---|---|
payment.unapplied_balance | the payment's own value minus what is already applied |
target.outstanding | the invoice or bill balance |
Because /apply is meant to be called repeatedly, allocations accumulate. A ₹5,000 receipt cannot be applied twice for ₹5,000 just because both invoices have room.
Pattern A — create and apply in one call
Use this when you already know the split. One round trip, one idempotency key, one transaction — the batch is validated up front, so you get a single 400 rather than a half-applied payment.
Request
POST /api/public/v1/payments
Host: api.books.finzko.com
Authorization: Bearer aibk_pat_xxxxxxxxxxxxxxxxxxxxxxxx
Idempotency-Key: rcpt-2026-07-27-ram-001
Content-Type: application/json
{
"flow": "INCOMING",
"contact_id": "8590d61f-7a71-4050-84f2-ef34b3a3f951",
"date": "2026-07-27T00:00:00",
"amount": 9733,
"mode": "UPI",
"payment_type": "REGULAR",
"reference_number": "620821345575",
"description": "UPI settlement 27 Jul",
"currency_code": "INR",
"exchange_rate": 1.0,
"bank_charges": 0.0,
"tds_amount": 0.0,
"bank_account_id": "b1f9c2e4-1a55-4c07-9f31-2d7a10c4e881",
"allocations": [
{ "invoice_id": "e1c8a0d2-...-856", "amount": 6195 },
{ "invoice_id": "a7b3f914-...-2358", "amount": 3538 }
]
}bank_account_id comes from GET /accounts filtered to type BANK or CASH. Omit it only in a single-bank org — multi-bank setups must specify, or the money lands in the wrong ledger.
Response — 201
{
"code": 0,
"message": "The payment has been created.",
"payment": {
"payment_id": "6d348a2e-bd90-4787-ac9c-fe07d6145526",
"flow": "INCOMING",
"payment_type": "REGULAR",
"contact_id": "8590d61f-7a71-4050-84f2-ef34b3a3f951",
"contact_name": "Ram",
"date": "2026-07-27T00:00:00",
"amount": 9733.0,
"mode": "UPI",
"reference_number": "620821345575",
"description": "UPI settlement 27 Jul",
"currency_code": "INR",
"exchange_rate": 1.0,
"bank_charges": 0.0,
"tds_amount": 0.0,
"bank_account_id": "b1f9c2e4-1a55-4c07-9f31-2d7a10c4e881",
"allocations": [
{ "allocation_id": "3052b92b-...", "invoice_id": "e1c8a0d2-...-856", "bill_id": null, "amount": 6195.0 },
{ "allocation_id": "9a71c4d0-...", "invoice_id": "a7b3f914-...-2358", "bill_id": null, "amount": 3538.0 }
],
"created_time": "2026-07-27T09:14:02.118Z",
"last_modified_time": "2026-07-27T09:14:02.118Z"
}
}Sum the allocations[].amount and subtract from amount to get the unapplied balance — here, zero.
Error — allocations exceed the payment
HTTP/1.1 400 Bad Request
{
"code": "validation.invalid_value",
"message": "Sum of allocations (12000) exceeds payment amount (9733).",
"request_id": "req_01J8Z3Q..."
}Nothing is created. Fix the split and resend with the same key.
Pattern B — record now, apply later
Use this when the money arrives before you know what it settles — a bank feed, a UPI sweep, a customer paying on account. Omit allocations entirely.
Step 1 — record the receipt as an advance
POST /api/public/v1/payments
Authorization: Bearer aibk_pat_xxxxxxxxxxxxxxxxxxxxxxxx
Idempotency-Key: rcpt-2026-07-27-ram-001
Content-Type: application/json
{
"flow": "INCOMING",
"contact_id": "8590d61f-7a71-4050-84f2-ef34b3a3f951",
"date": "2026-07-27T00:00:00",
"amount": 5000,
"mode": "UPI",
"payment_type": "ADVANCE",
"reference_number": "620821345575",
"bank_account_id": "b1f9c2e4-1a55-4c07-9f31-2d7a10c4e881"
}HTTP/1.1 201 Created
{
"code": 0,
"message": "The payment has been created.",
"payment": {
"payment_id": "6d348a2e-bd90-4787-ac9c-fe07d6145526",
"flow": "INCOMING",
"payment_type": "ADVANCE",
"contact_id": "8590d61f-7a71-4050-84f2-ef34b3a3f951",
"contact_name": "Ram",
"date": "2026-07-27T00:00:00",
"amount": 5000.0,
"mode": "UPI",
"allocations": [],
"created_time": "2026-07-27T09:14:02.118Z",
"last_modified_time": "2026-07-27T09:14:02.118Z"
}
}allocations: [] — the full ₹5,000 sits on the customer-advance liability ledger. Not on Accounts Receivable: A/R only ever carries what a payment actually settles.
Step 2 — apply it to an invoice
The body accepts exactly three fields and rejects anything else (extra: forbid): invoice_id or bill_id, plus amount. Idempotency-Key is required.
POST /api/public/v1/payments/6d348a2e-bd90-4787-ac9c-fe07d6145526/apply
Authorization: Bearer aibk_pat_xxxxxxxxxxxxxxxxxxxxxxxx
Idempotency-Key: apply-rcpt2337-gov856-001
Content-Type: application/json
{
"invoice_id": "e1c8a0d2-...-856",
"amount": 5000
}HTTP/1.1 200 OK
{
"code": 0,
"message": "The allocation has been recorded.",
"payment": {
"payment_id": "6d348a2e-bd90-4787-ac9c-fe07d6145526",
"flow": "INCOMING",
"payment_type": "ADVANCE",
"contact_id": "8590d61f-7a71-4050-84f2-ef34b3a3f951",
"contact_name": "Ram",
"amount": 5000.0,
"allocations": [
{ "allocation_id": "3052b92b-...", "invoice_id": "e1c8a0d2-...-856", "bill_id": null, "amount": 5000.0 }
],
"last_modified_time": "2026-08-24T04:53:04.902Z"
},
"allocation": {
"allocated_amount": 5000.0,
"round_off_amount": 0.0,
"invoice_id": "e1c8a0d2-...-856"
}
}The top-level allocation object tells you what this call did; payment.allocations[] is the running total. Read allocated_amount, not your requested amount — they differ when a round-off cap applies.
Step 3 — the call that used to succeed and now does not
The receipt is spent. The second invoice has ₹3,538 outstanding, so the target ceiling is satisfied — but the payment ceiling is not:
POST /api/public/v1/payments/6d348a2e-bd90-4787-ac9c-fe07d6145526/apply
Idempotency-Key: apply-rcpt2337-inv2358-001
{
"invoice_id": "a7b3f914-...-2358",
"amount": 5000
}HTTP/1.1 400 Bad Request
{
"code": "validation.invalid_value",
"message": "Amount 5000.00 exceeds this payment's unapplied balance 0.00. The payment is worth 5000.00 and 5000.00 is already applied.",
"details": {
"payment_amount": 5000.0,
"already_applied": 5000.0,
"unapplied_balance": 0.0,
"requested": 5000.0
},
"request_id": "req_01J8Z3Q..."
}Nothing is written when a call is refused — there is no partial state to unwind. Branch on details.unapplied_balance: retry with that amount if it is greater than zero, otherwise record another receipt.
Round-off: small overshoots are absorbed
Exceed a ceiling by no more than the organisation's paymentRoundOffTolerance (default ₹1.00) and the allocation is capped at the balance, with the remainder posted to the Round Off ledger. Beyond the tolerance it is a real mismatch and returns 400.
POST /payments/{payment_id}/apply
{ "invoice_id": "a7b3f914-...", "amount": 1180.50 } // invoice balance 1180.00HTTP/1.1 200 OK
{
"code": 0,
"message": "The allocation has been recorded. Rounded off ₹0.50 (within paymentRoundOffTolerance).",
"payment": { "...": "..." },
"allocation": {
"allocated_amount": 1180.0,
"round_off_amount": 0.5,
"invoice_id": "a7b3f914-..."
}
}Note allocated_amount is 1180.00, not 1180.50. See Round-off.
Retries are safe
Re-sending an allocation you already made is a no-op, not an error — so a timeout you retry cannot double-credit a customer. The same (payment, document, amount) returns 200 and writes nothing further:
{
"code": 0,
"message": "The allocation has been recorded.",
"allocation": {
"allocated_amount": 5000.0,
"round_off_amount": 0.0,
"invoice_id": "e1c8a0d2-...-856",
"already_applied": true
}
}This is separate from Idempotency-Key: the key protects the create, this protects the apply. Applying a different amount to a document you have already applied to is refused — that is an edit, and it must go through unapply so the ledger and the document move together:
HTTP/1.1 409 Conflict
{
"code": "conflict.already_allocated",
"message": "This payment is already applied to this invoice for 2,000.00. Unapply it first before applying a different amount.",
"details": {
"applied_amount": 2000.0,
"requested": 5000.0
}
}Invoices and bills answer identically — same status, same code. The conflict is checked beforethe two ceilings, so you get the actionable message (“unapply first”) rather than “the payment has nothing left”.
Reading a payment's position
GET /api/public/v1/payments/6d348a2e-bd90-4787-ac9c-fe07d6145526
Authorization: Bearer aibk_pat_xxxxxxxxxxxxxxxxxxxxxxxx{
"code": 0,
"message": "success",
"payment": {
"payment_id": "6d348a2e-bd90-4787-ac9c-fe07d6145526",
"amount": 9733.0,
"allocations": [
{ "allocation_id": "3052b92b-...", "invoice_id": "e1c8a0d2-...-856", "bill_id": null, "amount": 6195.0 }
]
}
}There is no unapplied_balance field. Derive it:
unapplied = payment.amount - sum(a.amount for a in payment.allocations)
# 9733.0 - 6195.0 = 3538.0That figure is also exactly the amount still sitting as an advance for this customer, so you do not need a second call to report their advance position.
Advances, in ledger terms
| State | Ledger effect |
|---|---|
| Receipt recorded, unallocated | Dr Bank · Cr Advances from Customers |
| Applied to an invoice | Dr Advances from Customers · Cr Accounts Receivable |
| Un-applied again | Dr Accounts Receivable · Cr Advances from Customers |
A partly-applied payment splits: the settled part is on A/R, the rest is an advance. There is no state in which a payment is “on account” in full while also reducing an invoice.
Unapply reverses posted vouchers with a mirror entry — it never deletes them, so the ledger keeps saying an application happened and was later undone. Do not try to correct an allocation by unapplying more than you applied; read the payment first and reconcile against the derived unapplied balance.
Integration checklist
- Send
Idempotency-Keyon every create and every apply. - Send
bank_account_idexplicitly if the org has more than one bank. - Read
allocation.allocated_amount, not your requested amount. - Treat
already_applied: trueas success. - On 400, branch on
details.unapplied_balancerather than re-sending blindly. - On 409
conflict.already_allocated, unapply before changing an amount — do not retry with a different figure. - Derive unapplied balance from
amount − Σ allocations; never assume it is zero. - Prefer Pattern A when the split is known — it is atomic.