Partial and off-platform payments
Take more than one payment on an invoice, record checks and wires paid outside Wingspan, fix or remove a recorded payment, and handle bounced checks and overpayments.
An invoice can take more than one payment. Wingspan keeps a running balance from every payment on it, whether the money came through Wingspan or you recorded a check or wire paid outside Wingspan. The invoice stays open until the balance reaches zero.
How the balance works
Every payment is an entry in the invoice's payments array. Three read-only fields on the invoice track the balance:
| Field | What it holds |
|---|---|
amountPaid | The total of the payments that count toward the invoice |
amountDue | amount minus applied credits minus amountPaid |
amountDueAfterCredits | amountDue minus credits waiting to apply. The next payment through Wingspan collects this. |
A payment counts while it's pending, processing or completed. A failed, cancelled or returned payment stops counting, and amountDue goes back up.
A partly paid invoice stays Opened, or Overdue once its due date passes. When amountDue reaches zero, the invoice moves to Paid. If every payment on it was recorded off platform, it moves to PaidOffPlatform instead.
Let clients pay part of an invoice
Your clients can pay part of an invoice through Wingspan when partial payments are on for them. Off-platform recordings and money Wingspan receives can always be partial, whatever the setting.
Set it at three levels. The most specific setting wins:
| Level | Where |
|---|---|
| One invoice | acceptsPartialPayment on Create an invoice or Update an invoice |
| One client | acceptsPartialPayment on Update a payer |
| Your Account | acceptsPartialPayment on Update payee settings. Off by default. |
Leave the invoice or client value empty to inherit from the level above. The invoice's effectiveAcceptsPartialPayment shows the result.
With partial payments on, Pay an invoice takes any amount above zero, up to amountDueAfterCredits. With them off, amount must equal amountDueAfterCredits. Leave amount out to collect the full balance. Autopay always collects the full balance.
Record a payment made outside Wingspan
A $1,200.00 invoice is open, and the client mails a $400.00 check. Record it with Record an off-platform payment:
curl -X POST "https://api.wingspan.app/v3/payments/invoices/Iv7Hn2Qx5Rt8Kw3Lp6Zb9D/pay-off-platform" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"externalId": "receipt-88412",
"amount": 400.00,
"paymentMethodDescription": "Check",
"paidDate": "2026-10-09",
"referenceNumber": "1042"
}'// (trimmed)
{
"status": "Opened",
"amount": 1200.00,
"amountPaid": 400.00,
"amountDue": 800.00,
"amountDueAfterCredits": 800.00,
"payments": [
{
"id": "Pm3Kx8Wq1Tz6Nv4Rb7Hm2A",
"source": "OffPlatform",
"method": "OffPlatform",
"status": "Completed",
"amount": 400.00,
"currency": "USD",
"externalId": "receipt-88412",
"paidDate": "2026-10-09",
"paymentMethodDescription": "Check",
"referenceNumber": "1042"
}
]
}The invoice now shows $800.00 due and stays Opened. Record the next check the same way, with a new externalId.
| Field | Required | Notes |
|---|---|---|
externalId | Yes | Your reference for this payment, such as a receipt ID from your ledger. Unique per invoice for you. |
amount | Yes | The part applied to this invoice. Up to amountDue, less credits waiting on this invoice. Credits on the client's balance don't apply to off-platform payments. |
paymentMethodDescription | Yes | How the client paid, such as Check or Wire. |
paidDate | Yes | The date the client paid, YYYY-MM-DD. Can't be in the future. |
referenceNumber | No | Check number or transfer reference. |
receivedAmount | No | The full amount you received, when it's more than amount. See Overpayments. |
notes | No | Free text. |
Send an Idempotency-Key with every recording. You only see your own externalId values. The client can record payments it made on its side of the same record with Record an off-platform payment for a payable, and each party manages only the entries it recorded.
One check, several invoices
When one check pays several invoices, record one entry on each invoice for the part applied there, and give them the same referenceNumber.
Retry safely
If you lose the response, send the request again with the same externalId and a new Idempotency-Key. Wingspan keeps the first response to each key for 24 hours and returns it again, errors included, so the old key can't tell you anything new. Leave out If-Match: if the first attempt went through, it changed the invoice's ETag, and the old value would fail with 412. Your externalId keeps the payment from being recorded twice.
If the payment was already recorded, you get 409 with code ResourceConflict. Read the invoice. An entry with your externalId means the payment is on the invoice. If there's no such entry, someone removed it, and you need a new externalId to record it again.
Edit or remove a recorded payment
You can change or remove the off-platform payments you recorded. The paymentId is the entry's id.
To fix an amount, send Edit an off-platform payment entry with the fields to change in updateMask. Say the $400.00 check was really $350.00:
curl -X PATCH "https://api.wingspan.app/v3/payments/invoices/Iv7Hn2Qx5Rt8Kw3Lp6Zb9D/payments/Pm3Kx8Wq1Tz6Nv4Rb7Hm2A" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H 'If-Match: "q3Xv9LmT2RbK7wN4cY8dP1sZ6hF"' \
-H "Content-Type: application/json" \
-d '{ "updateMask": ["amount"], "amount": 350.00 }'The invoice now shows $850.00 due. You can edit amount, receivedAmount, paidDate, paymentMethodDescription, referenceNumber, notes and metadata. The externalId can't change.
To take a payment off the invoice, send Remove an off-platform payment entry with If-Match. It returns 204, and the amount is due again. Remove the entry if a check you recorded bounces, or if you recorded a payment twice.
Both calls need the invoice's current ETag in If-Match. Any new payment or change on the invoice changes the ETag, so read the invoice first if anything else may have written to it. A stale ETag returns 412 and changes nothing.
| You try to change | Result |
|---|---|
| An off-platform payment you recorded | 200 for an edit, 204 for a removal |
| A payment the client recorded | 403 |
| A payment Wingspan collected or received | 409 |
Checks and transfers Wingspan receives
When a client mails a check to Wingspan or sends a wire or ACH transfer, Wingspan applies it to the invoice as a payment with source set to WingspanReceipt and method set to Check, Wire or Ach. You don't record these yourself.
A check clears like an ACH debit. If it bounces, its entry moves to Returned and stops counting, so amountDue goes back up. The invoice moves to Returned while a balance is due, and InvoicePayment.Returned fires. The client can pay again through Wingspan.
Overpayments
The balance never goes below zero. When a client pays more than is due, amount on the entry is the part applied to the invoice and receivedAmount is the full amount received.
For money Wingspan receives, Wingspan creates a credit for the excess, linked to the payment by sourcePaymentId. Wingspan pays you the full amount it received. The credit lowers the client's later invoices, or you can refund it, up to the excess.
For money paid outside Wingspan, record the payment with receivedAmount, then create the credit yourself. For a $1,500.00 wire against a $1,200.00 balance, record the payment:
// POST /v3/payments/invoices/Iv7Hn2Qx5Rt8Kw3Lp6Zb9D/pay-off-platform
{
"externalId": "receipt-90010",
"amount": 1200.00,
"receivedAmount": 1500.00,
"paymentMethodDescription": "Wire",
"paidDate": "2026-10-15",
"referenceNumber": "WIRE-6001"
}Then create a credit for the $300.00 excess, linked to the new entry's id:
// POST /v3/payments/credits
{
"payerId": "Pz4Rk8Wq2Tn6Lx9Vb3Mc1H",
"invoiceId": "Iv7Hn2Qx5Rt8Kw3Lp6Zb9D",
"amount": 300.00,
"currency": "USD",
"reason": "OverCollection",
"sourcePaymentId": "Pm8Wn2Kq5Rx1Tz7Lb4Hv6C",
"disposition": "Balance"
}The credit lowers the client's later invoices. Wingspan never held that money, so you can close the credit but can't refund it through Wingspan.
Editing or removing a payment doesn't change a credit linked to it. Change or close the credit through the Credits calls.
Webhooks
| Event | Fires when |
|---|---|
InvoicePayment.Updated | You edit a payment you recorded |
Invoice.PaymentRemoved | You remove a payment you recorded. context.paymentId names it. |
InvoicePayment.Returned | A payment comes back, such as a bounced check |
Invoice.Returned | The invoice moves to Returned after a payment comes back |
Invoice.PaidOffPlatform | Off-platform payments clear the balance |
Payable.PaymentRemoved fires on the client's side when the client removes a payment it recorded.
Errors
| Status | Code | Meaning |
|---|---|---|
422 | ValidationError | amount is zero or less, more than the balance, or more than receivedAmount, or a required field is missing. |
409 | ResourceConflict | You already used this externalId on this invoice. Read the invoice to see whether it's recorded. |
409 | InvalidStateTransition | The invoice can't take payments: it hasn't been sent, it's cancelled, or it's fully refunded. |
409 | ResourceLocked | Other writes kept getting there first. Retry with a new Idempotency-Key. |
412 | If-Match doesn't match the invoice's current ETag. Nothing changed. |
Related pages
Updated 13 days ago