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:

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.type is ExternalBankAccount or PaymentCard, and the instrument must belong to your Account.
  • method describes how the payee is paid, not what you're debited from. Only Ach is accepted, and you can omit it.
  • amount is optional. If you send it, it must equal the full payable amount.
  • shouldEnableAutoPay must be omitted or false.

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

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 codedetailCodeCause and fix
422 ValidationErrorpayments.PayableRelationshipRequiredNeither payeeId nor payeeEngagementId was sent. Send one.
422 ValidationErrorpayments.PayableRelationshipConflictBoth were sent. Send only one.
422 ValidationErrorpayments.PayableLineItemsRequiredlineItems is empty.
422 ValidationErrorpayments.PayableLineItemsInvalidA line amount or discount didn't validate. Check totalCost versus quantity and unitCost, and decimal places.
422 ValidationErrorpayments.PayableCurrencyUnsupported or payments.PayableCurrencyNotAllowedThe currency isn't supported, or isn't enabled for your Account.
422 ValidationErrorpayments.PayableEngagementInactive or payments.PayableEngagementTypeUnsupportedThe engagement is inactive or its type can't receive payables.
422 ValidationErrorpayments.PayablePayeeAccountMismatchpayeeAccountId doesn't match the payee. Drop it or correct it.
422 ValidationErrorpayments.PayablePayPeriodUnsupported, payments.PayableSourceWorkLogsUnsupportedThese fields aren't supported on create yet. Remove them.
422 ValidationErrorpayments.PayableBusinessRuleRejectedAnother business rule rejected the payable. Contact support with the requestId.
409 ResourceConflictpayments.PayableExternalIdConflictThe externalId is already used on another payable. Look it up instead of creating it again.
409 EligibilityBlockedpayments.EngagementNotPaymentsEligibleThe payee's engagement has incomplete requirements. Returned by open, pay, send, remind, and pay-off-platform.
409 ResourceConflictpayments.FundingSourceRequiredNo usable funding source could be selected for pay.
404 ResourceNotFoundOn pay, the funding source doesn't exist or isn't yours.
409On pay, a retry named a different funding source than the first request.
403 StepUpMfaRequiredComplete the MFA challenge in extensions.challengeUri and retry.
412 PreconditionFailedYour If-Match is stale. Read the payable and retry.

Next steps


Did this page help you?