Refund an invoice
Give a client back money it paid on an invoice through Wingspan, in full or in part, follow the refund until it settles, and choose between a refund, a credit, and a void.
A refund sends money your client paid through Wingspan back to the card or bank account it came from. You can refund all of it or part of it, and you can refund the same invoice more than once until nothing refundable is left. For a payable you were paid, see Refunds and returns.
When you can refund
You can refund an invoice once your client has paid it in full through Wingspan and Wingspan has confirmed the deposit. Check events.depositedAt on the invoice, or wait for the Invoice.DepositConfirmed webhook.
| How the client paid | Where the refund goes |
|---|---|
| Card, saved or through a payment link | Back to the same card |
| Bank debit, through a mandate or a payment link | Back to the same bank account |
Check, cash, or a wire you recorded with pay-off-platform | Not through Wingspan. Pay it back yourself, then record a credit with disposition: OffPlatform |
To refund what was paid on a partly paid invoice, void the invoice first. Once it's voided, nothing more is due and you can refund the completed payments. Wait until no payment on it is Pending or Processing, because Wingspan refuses a void while one is.
Refund an invoice
Call Refund an invoice. Send amount for a partial refund, or leave it out to refund everything still refundable. reason is optional and only you see it.
curl -X POST "https://api.wingspan.app/v3/payments/invoices/Iv7Hn2Qx5Rt8Kw3Lp6Zb9D/refund" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: Iv7Hn2Qx5Rt8Kw3Lp6Zb9D-refund-1" \
-d '{
"amount": 150.00,
"reason": "Billed one extra shift"
}'// 200 OK (trimmed)
{
"id": "Iv7Hn2Qx5Rt8Kw3Lp6Zb9D",
"status": "Paid",
"amount": 1200.00,
"refunds": [
{
"id": "Rf4Kq9Wn2Lx7Tz5Bm8Hc3V",
"amount": 150.00,
"currency": "USD",
"status": "Processing",
"destination": { "type": "PaymentCard" },
"reason": "Billed one extra shift",
"events": { "initiatedAt": "2026-10-20T15:02:11Z" }
}
]
}The response is the invoice with the new refund in refunds[]. A 200 means Wingspan accepted the refund. It doesn't mean the money has reached your client yet.
Send an Idempotency-Key with every refund. If you lose the response, resend the same body with the same key and you get the same result back, with no second refund. A different body with the same key returns 409 IdempotencyKeyConflict.
Permissions and step-up
A refund moves money, so the call needs two things:
- The
payments.invoice-refund:writescope. Without it, the call returns403withcode: Forbidden. - If you call with a person's login session, a recent MFA step-up. Without it, the call returns
403withcode: StepUpMfaRequired. Create a challenge withPOST /v3/platform/mfa-challengesand{ "requiredFor": "HighRiskWriteAction" }, verify it atPOST /v3/platform/mfa-challenges/{challengeId}/verifywith the code from your authenticator, then retry the refund. Step-up proof is short-lived, so verify right before you refund. A backend calling with an API key has no session to step up. See Environments and authentication.
How much you can refund
The most you can refund is what Wingspan collected on the invoice, minus refunds you've already made. A refund counts against that limit as soon as Wingspan accepts it. If a refund fails, its amount becomes refundable again. A request above the limit returns 422.
Wingspan funds the refund from your Wingspan balance or from the bank account your payouts go to. If neither can cover it, the call returns 422.
If you used a credit on the invoice, refunding all of the cash collected gives the credit back once, so it can apply to your client's other invoices. A partial refund doesn't give credit back. See Credits.
Follow the refund
Read the invoice with GET /v3/payments/invoices/{invoiceId} to follow each refund. Every refund is one entry in refunds[]:
| Field | What it holds |
|---|---|
id | The refund's ID |
amount, currency | How much is going back |
status | Pending, Processing, Completed, Failed, or Cancelled |
destination.type | Where the money goes, such as PaymentCard or ExternalBankAccount. The card or account itself isn't shown. |
events | initiatedAt, completedAt, failedAt, cancelledAt, and returnedAt |
reason | Your reason. Your client doesn't see it. |
fundsMovementId | The money movement that carries the refund, once there is one |
When a refund is Completed, it also counts in the invoice's amountDetails.refunds, a negative total of settled refunds. amountDetails is left out while any refund or payment on the invoice is still settling.
The invoice keeps its status after a refund. A fully refunded invoice still reads Paid, so look at refunds[] and amountDetails.refunds to see what went back. Your client sees the refund on its side of the same record, without your reason.
If a refund fails, its entry stays in refunds[] with status: Failed, and the amount is refundable again. Send a new refund with a new Idempotency-Key.
Refund an overpayment
When a client sends more than an invoice needed, Wingspan applies what's due and keeps the excess as a credit linked to the payment. Refund the excess through that credit with POST /v3/payments/credits/{creditId}/complete and "remainderDisposition": "Refund". It needs the same scope and step-up as an invoice refund. See Partial and off-platform payments.
Wingspan can't refund money paid outside Wingspan, because it never held that money. Close that credit instead.
Refund, credit, void, or return
| You want to | Use |
|---|---|
| Give back money a client paid through Wingspan | A refund |
| Lower what a client owes without moving money | A credit with disposition: Balance, which applies to the client's next invoices |
| Fix the line items before the client pays | Update the invoice while it's Created or Opened |
| Cancel an invoice you sent | Void the invoice. Delete only drafts. |
| Record money you paid back by check | A credit with disposition: OffPlatform |
A return is different from a refund. A return is the bank sending a payment back, for example for insufficient funds. You don't start it. The payment entry moves to Returned with a statusReason, and Invoice.Returned fires. See Collect payment on an invoice.
Errors
| Status | Code | Meaning |
|---|---|---|
403 | StepUpMfaRequired | With a login session, verify a HighRiskWriteAction challenge, then retry. |
403 | Forbidden | Your credential doesn't have payments.invoice-refund:write. |
404 | NotFound | The invoice doesn't exist, or you didn't issue it. |
409 | ResourceConflict | Another refund on this invoice is still in progress with different details, or the invoice isn't paid and deposited yet. Wait for the refund or the deposit, then retry. |
409 | IdempotencyKeyConflict | You reused an Idempotency-Key with a different body. |
422 | ValidationError | The amount is zero or above what's refundable, the invoice isn't paid and deposited yet, it was paid off-platform, or your account can't fund the refund. |
Related pages
Updated 11 days ago