Create a payable
Create, open, approve, and pay a single payable with the Wingspan V3 API, with example requests and the errors you might see.
This guide walks you through paying one payee: create a payable, open it, approve it, and pay it. It also covers editing a payable and recording a payment you made outside Wingspan.
Use a single payable for occasional payments or anything that needs individual review. For many payees at once, see Bulk payables and Payroll runs.
Before you begin
You need:
- An API token. Every request sends
Authorization: Bearer $WINGSPAN_TOKEN. See Environments and authentication. - A Payee to pay, and its
id. See Invite a payee. If the payee has more than one engagement with you, also note thepayeeEngagementIdyou want to pay under. - A payee engagement that's payments eligible. Check with
GET /v3/payments/payee-engagements/{payeeEngagementId}:paymentsEligibilityshould beEligible. See Requirements and eligibility. - A saved funding instrument if you'll pay directly: a verified external bank account or a verified payment card owned by your Account. See Payment and payout methods.
If you're acting for a child Account, add X-Wingspan-Account: <accountId> to every request. See Acting on behalf of Accounts.
Step 1: Create the payable
Call POST /v3/payments/payables with the payee, currency, due date, and at least one line item.
curl -X POST "https://api.wingspan.app/v3/payments/payables" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"payeeId": "q7Lm2VxR9tKd4WnB8sHc1Z",
"currency": "USD",
"dueDate": "2026-10-09",
"externalId": "NW-AP-20431",
"lineItems": [
{
"description": "Route coverage, September",
"lineItemType": "Services",
"quantity": 40,
"unitCost": 46.00,
"unit": "hours"
}
]
}'The response is 201 Created with the new payable and an ETag header. Save both.
// (trimmed)
{
"id": "Hb4Xk9Qw2Rt7Vn1Mz8Pc3L",
"accountId": "Aq9Zt4Mx1Kc7Rv3Ln8Pw2B",
"payeeId": "q7Lm2VxR9tKd4WnB8sHc1Z",
"payeeEngagementId": "Tz3Nq8KpW1vXr6Ld0Ya5Mc",
"externalId": "NW-AP-20431",
"status": "Created",
"payerApprovalStatus": "Pending",
"currency": "USD",
"amount": 1840.00,
"dueDate": "2026-10-09",
"events": { "createdAt": "2026-09-24T15:02:11Z" }
}Idempotency-Key makes a retry safe. If your request times out, resend it with the same key and body, and you get the original result instead of a second payable. The response cache lasts 24 hours and is best effort, so also rely on externalId: a duplicate returns 409 ResourceConflict, and you can find the existing payable with GET /v3/payments/payables?filter[externalId][eq]=NW-AP-20431. See Idempotency.
For every field you can send, see Components of a payable.
Step 2: Open the payable
Opening finalizes the payable and makes it visible to the payee. Send the ETag from step 1 as If-Match.
curl -X POST "https://api.wingspan.app/v3/payments/payables/Hb4Xk9Qw2Rt7Vn1Mz8Pc3L/open" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'If-Match: "3q2-7wXbTn0kLr8Hc1VxMzQaPdE"'// (trimmed)
{
"id": "Hb4Xk9Qw2Rt7Vn1Mz8Pc3L",
"status": "Opened",
"invoiceNumber": "NW-0042",
"events": { "createdAt": "2026-09-24T15:02:11Z", "openedAt": "2026-09-24T15:03:40Z" }
}If the payee's engagement still has incomplete requirements, you get 409 with code: EligibilityBlocked and detailCode: payments.EngagementNotPaymentsEligible. The payable stays Created. Help the payee finish their requirements, then open it again.
To let the payee know, call POST /v3/payments/payables/{payableId}/send.
Step 3: Approve the payable
Mark the payable Approved with PATCH /v3/payments/payables/{payableId}/workflow-status.
curl -X PATCH "https://api.wingspan.app/v3/payments/payables/Hb4Xk9Qw2Rt7Vn1Mz8Pc3L/workflow-status" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "payerApprovalStatus": "Approved" }'// (trimmed)
{ "id": "Hb4Xk9Qw2Rt7Vn1Mz8Pc3L", "status": "Opened", "payerApprovalStatus": "Approved" }At this point you can stop and let a payroll run pay it with your other approved payables. To pay it on its own now, continue to step 4.
Step 4: Pay the payable
Call POST /v3/payments/payables/{payableId}/pay. Idempotency-Key is required here, and fundingSource names the saved instrument to debit.
curl -X POST "https://api.wingspan.app/v3/payments/payables/Hb4Xk9Qw2Rt7Vn1Mz8Pc3L/pay" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: nw-pay-NW-AP-20431" \
-H "Content-Type: application/json" \
-d '{
"fundingSource": { "type": "ExternalBankAccount", "id": "Fs6Lp1Rk9Vx3Nq7Tz2Wb5H" },
"method": "Ach"
}'// (trimmed)
{
"id": "Hb4Xk9Qw2Rt7Vn1Mz8Pc3L",
"status": "PaymentInTransit",
"payments": [
{ "id": "Ia2Mv8Kx4Rq9Tw1Lz6Nb3F", "method": "Ach", "status": "Processing", "amount": 1840.00, "currency": "USD" }
]
}fundingSource.typeisExternalBankAccountorPaymentCard, and the instrument must belong to your Account.methoddescribes how the payee is paid, not what you're debited from. OnlyAchis accepted, and you can omit it.amountis optional. If you send it, it must equal the full payable amount.shouldEnableAutoPaymust be omitted orfalse.
The first accepted request locks the amount, funding source, and payout routes. Retrying with the same key is safe. Retrying with a different funding source returns 409.
This call is a high-risk action. If your session hasn't completed recent multi-factor authentication, it returns 403 with code: StepUpMfaRequired and a challenge link in extensions. See Errors.
A payee doesn't need a linked Wingspan Account to be paid. Wingspan routes the payout using the payee's payout settings.
Step 5: Confirm the payment
The payable moves to Paid when Wingspan's internal state records the payment. To confirm without polling, subscribe to the Payable.Paid and Payable.Returned webhooks and look up the payable by the data.id in each event. See Create a webhook subscription.
curl "https://api.wingspan.app/v3/payments/payables/Hb4Xk9Qw2Rt7Vn1Mz8Pc3L" \
-H "Authorization: Bearer $WINGSPAN_TOKEN"// (trimmed)
{
"id": "Hb4Xk9Qw2Rt7Vn1Mz8Pc3L",
"status": "Paid",
"events": {
"openedAt": "2026-09-24T15:03:40Z",
"paymentInTransitAt": "2026-09-24T15:06:02Z",
"paidAt": "2026-09-24T15:06:05Z"
},
"payouts": [ { "id": "Po4Tz7Kq2Xm9Lw1Rv5Nb8H", "status": "Processing", "amount": 1840.00, "currency": "USD" } ]
}Paid is Wingspan's internal state. DepositConfirmed means Wingspan's originating bank or provider reported its terminal processed state. It never means the recipient's bank received the money, funds are available, or the payment can't be returned. Watch events.depositConfirmedAt and the payouts[] status for the payout leg. See Paid vs DepositConfirmed.
Edit a payable
You can edit a Created or Opened payable with PATCH /v3/payments/payables/{payableId}. Send only the fields you're changing: lineItems, dueDate, scheduledPaymentDate, notes, metadata, or quickbooks. Send null for scheduledPaymentDate to clear it. If-Match is required.
curl -X PATCH "https://api.wingspan.app/v3/payments/payables/Hb4Xk9Qw2Rt7Vn1Mz8Pc3L" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'If-Match: "3q2-7wXbTn0kLr8Hc1VxMzQaPdE"' \
-H "Content-Type: application/json" \
-d '{ "dueDate": "2026-10-16", "notes": "Due date moved to match client billing" }'If someone changed the payable after you read it, you get 412 with code: PreconditionFailed and the current ETag. Read the payable again, check the change still makes sense, and retry. See Concurrency and ETags.
Record a payment made outside Wingspan
If you paid by check, wire, or cash, record it so your payment history and tax records stay complete. Call POST /v3/payments/payables/{payableId}/pay-off-platform:
curl -X POST "https://api.wingspan.app/v3/payments/payables/Hb4Xk9Qw2Rt7Vn1Mz8Pc3L/pay-off-platform" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 1840.00,
"paymentMethodDescription": "Check",
"paidDate": "2026-09-30",
"referenceNumber": "CHK-10233"
}'The payable moves to PaidOffPlatform. This records the payment only; no money moves. A payable with splits returns 409.
Cancel or delete
- Delete a
Createdpayable withDELETE /v3/payments/payables/{payableId}. Any other status returns409. - Cancel an opened, unpaid payable with
POST /v3/payments/payables/{payableId}/cancel. You can send an optionalreason. APaidorCancelledpayable returns409.
What can go wrong
Branch on code. detailCode tells you more for logs and support, but it can grow over time. See Errors.
Status and code | detailCode | Cause and fix |
|---|---|---|
422 ValidationError | payments.PayableRelationshipRequired | Neither payeeId nor payeeEngagementId was sent. Send one. |
422 ValidationError | payments.PayableRelationshipConflict | Both were sent. Send only one. |
422 ValidationError | payments.PayableLineItemsRequired | lineItems is empty. |
422 ValidationError | payments.PayableLineItemsInvalid | A line amount or discount didn't validate. Check totalCost versus quantity and unitCost, and decimal places. |
422 ValidationError | payments.PayableCurrencyUnsupported or payments.PayableCurrencyNotAllowed | The currency isn't supported, or isn't enabled for your Account. |
422 ValidationError | payments.PayableEngagementInactive or payments.PayableEngagementTypeUnsupported | The engagement is inactive or its type can't receive payables. |
422 ValidationError | payments.PayablePayeeAccountMismatch | payeeAccountId doesn't match the payee. Drop it or correct it. |
422 ValidationError | payments.PayablePayPeriodUnsupported, payments.PayableSourceWorkLogsUnsupported | These fields aren't supported on create yet. Remove them. |
422 ValidationError | payments.PayableBusinessRuleRejected | Another business rule rejected the payable. Contact support with the requestId. |
409 ResourceConflict | payments.PayableExternalIdConflict | The externalId is already used on another payable. Look it up instead of creating it again. |
409 EligibilityBlocked | payments.EngagementNotPaymentsEligible | The payee's engagement has incomplete requirements. Returned by open, pay, send, remind, and pay-off-platform. |
409 ResourceConflict | payments.FundingSourceRequired | No usable funding source could be selected for pay. |
404 ResourceNotFound | On pay, the funding source doesn't exist or isn't yours. | |
409 | On pay, a retry named a different funding source than the first request. | |
403 StepUpMfaRequired | Complete the MFA challenge in extensions.challengeUri and retry. | |
412 PreconditionFailed | Your If-Match is stale. Read the payable and retry. |
Next steps
- Payroll runs to pay many approved payables at once
- Payable lifecycle and statuses
- Process a payable for an end-to-end recipe
Updated 10 days ago