Invoice lifecycle

Every V3 invoice status, the action calls that move an invoice between them, when you can edit, and which webhooks you can subscribe to.

This page explains each status an invoice can be in, which call or event moves it there, and what
you can still change at each step.

Why status changes are action calls

You never change an invoice's status with PATCH. Each change is its own POST
(/send, /void, /pay, and so on). That way the API knows what happened, not only the result,
and records who did it and when in events and actors. PATCH only edits fields.

Statuses

Created corresponds to V1's Draft, and Opened to V1's Open.

StatusMeaningHow it gets there
CreatedThe invoice exists but hasn't been sent. You can edit or delete it.POST /v3/payments/invoices
OpenedSent to the payer and waiting for payment.POST .../send
OverdueThe dueDate passed without payment.Wingspan, automatically
PendingPayment can't go ahead yet because a condition isn't met, such as outstanding eligibility requirements on the engagement.Wingspan
DisputedThe invoice is under dispute by the payee or the payer.Wingspan, when a dispute is recorded
PaymentInTransitA payment has been started and is being processed.POST .../pay, or a payer payment
PartiallyPaidSome of the payouts to the invoice's split recipients failed. It doesn't mean part of the invoice is paid.Wingspan, from split payout processing
PaidWingspan's system has marked the invoice paid.Wingspan
PaidOffPlatformPayment happened outside Wingspan and was recorded.POST .../pay-off-platform
ReturnedThe payment rail returned the payment.Wingspan, from the bank or provider
PartiallyRefundedPart of the payment was refunded.POST .../refund with an amount
RefundedThe full payment was refunded.POST .../refund
CancelledThe invoice was voided.POST .../void

An invoice paid in part stays Opened or Overdue until the rest is paid, and its amountPaid and amountDue show what's been paid and what's left. See Partial and off-platform payments.

stateDiagram-v2
    [*] --> Created
    Created --> Opened: send
    Created --> [*]: DELETE
    Opened --> Overdue: due date passes
    Opened --> PaymentInTransit: pay
    Overdue --> PaymentInTransit: pay
    Opened --> PaidOffPlatform: pay-off-platform
    Overdue --> PaidOffPlatform: pay-off-platform
    Opened --> Cancelled: void
    Overdue --> Cancelled: void
    PaymentInTransit --> Paid
    PaymentInTransit --> PartiallyPaid: split payout partly fails
    PaymentInTransit --> Returned
    Paid --> Refunded: refund
    Paid --> PartiallyRefunded: refund (partial)

The diagram shows the common paths. Wingspan can also move an invoice between states on its own,
for example when a payment fails and can be retried.

Actions

CallWho uses itAllowed whenNotes
PATCH /v3/payments/invoices/{invoiceId}PayeeCreated or OpenedOmitted fields stay the same. Send null to clear a nullable field. Splits can't be changed after sending.
DELETE /v3/payments/invoices/{invoiceId}PayeeCreated onlyReturns 204. On any other status it returns 409. Use void instead.
POST .../sendPayeeCreatedMoves to Opened and emails the payer.
POST .../remindPayeeOpened or OverdueEmails a payment reminder.
POST .../voidPayeeAfter sendingCancels a sent invoice. A reason field isn't supported and returns 422.
POST .../acceptPayeeOpened or OverdueFor an obligation the payer created: confirms you agree with the amount and terms before payment starts. Sets payeeReviewStatus: Accepted.
POST .../disputePayeeOpened or OverdueRecords your dispute and sets payeeReviewStatus: Disputed. Requires a non-empty reason. Retrying with the same reason returns the existing dispute unchanged. A different reason on an existing dispute returns 409.
POST .../payPayee or linked payerOpened or OverdueCollects payment. See Collect payment.
POST .../pay-off-platformPayeeUnpaidRecords a full payment made outside Wingspan and marks the invoice PaidOffPlatform. To record part of the amount, see Partial and off-platform payments.
POST .../refundPayeePaid and depositedFull refund, or partial with amount. See Refunds and returns.

All paths are under /v3/payments/invoices/{invoiceId}.

Errors you'll see on transitions

Status and codeCause
409 InvalidStateTransitionThe invoice is in the wrong status for this action, for example accept on a disputed invoice.
409 EligibilityBlockedsend, remind, or pay-off-platform on an engagement whose payment eligibility requirements aren't complete. The detailCode is payments.EngagementNotPaymentsEligible. See Requirements and eligibility.
409 ResourceConflictA duplicate externalId on create.
409 (other)DELETE on an invoice that isn't Created, or a pay-off-platform retry with different payment details.
412 PreconditionFailedpay with an If-Match value that no longer matches the invoice. Read the invoice again and retry with the new ETag.

Branch on code. detailCode is there to help you debug and may change. See
Errors.

Review and approval run separately from status

Two more fields track workflow that sits beside status:

  • payerApprovalStatus (Pending, PreApproved, Approved, Declined) is the payer's approval
    of the obligation. The payer manages it from its side as a payable. See
    Payable lifecycle and statuses.
  • payeeReviewStatus (Pending, Accepted, Disputed, Resubmitted) is your review as the
    payee, set by accept and dispute.

Neither one moves money by itself.

Webhooks

Use invoice events to track collection. See Event types for routing scopes and payload details.

EventFires when
Invoice.OpenedThe invoice entered the issued, awaiting-payment lifecycle state.
Invoice.OverdueThe invoice recorded an Overdue transition.
Invoice.CancelledThe invoice was cancelled or voided.
Invoice.PaymentInTransitA payment has been started.
Invoice.PaidWingspan's system marked the invoice paid.
Invoice.DepositConfirmedWingspan's originating bank or provider reported its final processed status for the payment.
Invoice.PaymentFailedA payment attempt failed. The invoice can be paid again.
Invoice.ReturnedThe payment was returned by the rail.
InvoicePayment.Initiated, .Processing, .Completed, .Failed, .Returned, .CancelledEach collection attempt moves through its own status.
Invoice.RefundedThe invoice recorded a Refunded lifecycle transition.
Invoice.RefundInitiated, .RefundCompleted, .RefundFailed, .RefundReturned, .RefundCancelledA refund recorded its corresponding transition; context.refundId identifies that refund.

You can subscribe to all of these events, including Invoice.Opened, Invoice.Overdue, Invoice.Cancelled, Invoice.Refunded, and the Invoice.Refund* events. You can also read the invoice to see each transition.

Invoice.Opened describes issuance or reopening, not the payer viewing the invoice, email delivery, or a newly sent message. Invoice.Cancelled concerns the invoice itself; deleting a draft or receiving InvoicePayment.Cancelled is a different action.

Once the refund events are published, they will carry the invoice ID in data.id and the refund ID in context.refundId. They distinguish multiple refunds against one invoice. Use Invoice.RefundCompleted for the refund's completed transition; neither Invoice.RefundInitiated, aggregate Invoice.Refunded, nor the refund action's HTTP success proves cash movement completed. Refunds and credit adjustments are separate operations.

Invoice.Paid and Invoice.DepositConfirmed are different signals. Paid is Wingspan's internal
state. DepositConfirmed means Wingspan's originating bank or provider reported its terminal
processed state. It doesn't mean the recipient's bank received the money, that funds are available,
or that the payment can't be returned, so an Invoice.Returned can still follow. See
Paid vs DepositConfirmed.

Read the invoice with GET /v3/payments/invoices/{invoiceId} using the event's data.id to reconcile
its current status, amounts, and timestamps. The event is a reference to a transition; it does not
replace the current resource representation.

Webhook delivery is best effort and can send duplicates. Dedupe on the event id, and recover
anything you missed from the event log. See Webhooks overview
and Recover missed events.

Related pages


Did this page help you?