Create an invoice

Create a Payer, create an invoice with line items, attach a file, send it to your client, and fetch or share the PDF with the V3 API.

This guide takes you from nothing to a sent invoice: create a record for your client, create the
invoice, attach a supporting file, and email it. The example is a contractor, Priya Shah, billing
her client Northwind Staffing for 40 hours of design work plus a reimbursable expense.

In V1 you did this with GET /payments/memberClient and POST /payments/invoice. In V3 the client
is a Payer record and the invoice is created at POST /v3/payments/invoices.

Before you begin

  • An API token for the Account that issues the invoice (the payee). See
    Environments and authentication. The
    examples use production (https://api.wingspan.app). Use https://stagingapi.wingspan.app for
    testing.
  • If you're a platform issuing invoices for a child Account, add X-Wingspan-Account: <accountId>
    to every request. See Acting on behalf of Accounts.
  • Your client's billing email and how you want to bill (flat amount, hourly, or per unit).
  • Optional: read your client's invoicing policy first with
    GET /v3/payments/invoicing-configs/payers/{payerAccountId} if they have a Wingspan Account. It
    tells you which currencies, due dates, and line items they accept.

Send an Idempotency-Key on every create so a retried request doesn't create a second invoice. See
Idempotency.

1. Create a Payer

Skip this step if you already have a Payer for this client. Look it up by your own ID with
GET /v3/payments/payers?filter[externalId][eq]=CRM-1042.

Call POST /v3/payments/payers. Only
email is required.

curl -X POST https://api.wingspan.app/v3/payments/payers \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "email": "[email protected]",
    "externalId": "CRM-1042",
    "profile": { "doingBusinessAs": "Northwind Staffing" }
  }'
// 201 Created (trimmed)
{
  "id": "Pq7Lm2Xv9RtK4sWb8NcY3d",
  "email": "[email protected]",
  "externalId": "CRM-1042",
  "payerAccountId": null
}

Save id. That's the payerId you bill. payerAccountId stays null until your client links
its own Wingspan Account, which is optional. See
Payee and payer account linking.

Tip: Email is not a lookup key. If a Payer with that email already exists, you may get
409 ResourceConflict, or a 201 that returns the existing Payer unchanged. Handle both. Use
externalId to find your own records.

2. Create the invoice

Call POST /v3/payments/invoices.
currency, lineItems, dueDate, and exactly one of payerId, payerEngagementId, or
payeeEngagementId are required.

curl -X POST https://api.wingspan.app/v3/payments/invoices \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "payerId": "Pq7Lm2Xv9RtK4sWb8NcY3d",
    "externalId": "PS-2026-014",
    "currency": "USD",
    "dueDate": "2026-10-31",
    "lineItems": [
      {
        "description": "Website redesign, phase 2",
        "lineItemType": "Services",
        "quantity": 40,
        "unit": "hours",
        "unitCost": 95.00
      },
      {
        "description": "Stock photo license",
        "lineItemType": "Reimbursement",
        "totalCost": 120.00
      }
    ],
    "notes": "Thanks for the project. Payment terms are net 30.",
    "acceptedPaymentMethods": ["Ach", "Credit"],
    "creditFeeHandling": { "payerFeePercentage": 100, "payeeFeePercentage": 0 },
    "lateFeeHandling": {
      "lateFeePercentage": 1.5,
      "frequency": { "interval": "LateFeeIntervalMonthly", "every": 1 }
    },
    "notificationPreferences": {
      "shouldSendInvoice": true,
      "shouldSendReceipt": true,
      "shouldSendReminders": true
    },
    "metadata": { "project": "Northwind site redesign", "poNumber": "PO-88213" }
  }'
// 201 Created (trimmed)
{
  "id": "Hk3nV8qTz1Lw6Rb0Ym5Jcx",
  "accountId": "Ax9Tq2Lm7Vb4Nc1Rz8Kd5w",
  "payerId": "Pq7Lm2Xv9RtK4sWb8NcY3d",
  "externalId": "PS-2026-014",
  "status": "Created",
  "currency": "USD",
  "amount": 3920.00,
  "dueDate": "2026-10-31",
  "events": { "createdAt": "2026-09-24T15:02:11Z" }
}

The invoice is now Created. The payer can't see it yet, and you can still change or delete it.
Every field is described in Components of an invoice.

To fix something before sending, call
PATCH /v3/payments/invoices/{invoiceId}
with only the fields you're changing:

curl -X PATCH https://api.wingspan.app/v3/payments/invoices/Hk3nV8qTz1Lw6Rb0Ym5Jcx \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "dueDate": "2026-11-15" }'

To throw away a draft, call
DELETE /v3/payments/invoices/{invoiceId}.

3. Attach a supporting file (optional)

Files go to the vault first, and the invoice refers to them by ID. Upload with
POST /v3/compliance/vault-files (see Files and documents),
then attach the returned ID with
POST /v3/payments/invoices/{invoiceId}/attachments:

curl -X POST https://api.wingspan.app/v3/payments/invoices/Hk3nV8qTz1Lw6Rb0Ym5Jcx/attachments \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "memberFileId": "Jv1Qz6Kn3Xw8Lt5Rb2Mp7c", "caption": "Stock photo receipt" }'
// 201 Created (trimmed)
{
  "id": "Tv6Kq1Xn8Lz3Wb5Rm2Pc9d",
  "memberFileId": "Jv1Qz6Kn3Xw8Lt5Rb2Mp7c",
  "caption": "Stock photo receipt"
}

Attaching a private file gives your client ongoing read access to it, even if you detach it later.
List attachments with GET .../attachments and remove one with
DELETE .../attachments/{attachmentId}.

4. Send the invoice

Call POST /v3/payments/invoices/{invoiceId}/send.
It moves the invoice to Opened and emails it to the payer.

curl -X POST https://api.wingspan.app/v3/payments/invoices/Hk3nV8qTz1Lw6Rb0Ym5Jcx/send \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"
// 200 OK (trimmed)
{
  "id": "Hk3nV8qTz1Lw6Rb0Ym5Jcx",
  "status": "Opened",
  "events": {
    "createdAt": "2026-09-24T15:02:11Z",
    "openedAt": "2026-09-24T15:10:42Z"
  }
}

If the invoice carries collaborator splits, sending it also creates one child payable per split.
Splits can't be changed after this point.

5. Confirm it worked

Read the invoice back with GET /v3/payments/invoices/{invoiceId}
and check that status is Opened and events.openedAt is set. To see everything you've sent to
this client, list with a filter:

curl "https://api.wingspan.app/v3/payments/invoices?filter[status][eq]=Opened&page[size]=50" \
  -H "Authorization: Bearer $WINGSPAN_TOKEN"

Keep paging with page[token] until pagination.nextPageToken is "". See
Pagination.

Fetch or share the PDF

  • Download it yourself: GET /v3/payments/invoices/{invoiceId}/pdf returns 302 with a
    Location header pointing to a short-lived signed URL. Follow the redirect (curl -L) to get the
    file. For a paid invoice, GET .../receipt-pdf works the same way for the receipt.
  • Hand someone a link: POST /v3/payments/invoices/{invoiceId}/secure-links
    returns { id, url, format, expiresAt, sentTo, createdAt }. expiresIn must be 1 to 300
    seconds. The link expires on its own and can't be revoked separately.
  • Give an unlinked client a view of the invoice: the payerInvoiceViewToken on the invoice
    (returned only to you) grants read access to a limited view at
    GET /v3/payments/payer-invoice-view/{opaqueToken}, with no login. Treat the token as a secret.
    To let that client pay online, create a payment link. See Collect payment.

What can go wrong

ResponseCauseFix
422 ValidationErrorA required field is missing, both totalCost and quantity/unitCost are set on a line, two mutually exclusive fields are set (ConflictingFields), an amount has too many decimal places, or you sent a field that isn't supported (such as a line-item externalId).Read errors[] for the field and fix the request.
422 ValidationErrorNone, or more than one, of payerId, payerEngagementId, payeeEngagementId.Send exactly one.
409 ResourceConflictThe externalId is already used on another of your invoices.Look up the existing invoice with filter[externalId][eq].
409 EligibilityBlocked on sendThe engagement's payment eligibility requirements aren't complete (detailCode: payments.EngagementNotPaymentsEligible).See Requirements and eligibility.
409 on DELETEThe invoice was already sent.Void it with POST .../void.
404 ResourceNotFoundThe Payer or engagement doesn't exist, or you can't see it.Check the ID and the X-Wingspan-Account you're acting as.

See Errors for the error format.

Next steps


Did this page help you?