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). Usehttps://stagingapi.wingspan.appfor
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 a201that returns the existing Payer unchanged. Handle both. Use
externalIdto 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}/pdfreturns302with a
Locationheader pointing to a short-lived signed URL. Follow the redirect (curl -L) to get the
file. For a paid invoice,GET .../receipt-pdfworks the same way for the receipt. - Hand someone a link:
POST /v3/payments/invoices/{invoiceId}/secure-links
returns{ id, url, format, expiresAt, sentTo, createdAt }.expiresInmust 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
payerInvoiceViewTokenon 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
| Response | Cause | Fix |
|---|---|---|
422 ValidationError | A 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 ValidationError | None, or more than one, of payerId, payerEngagementId, payeeEngagementId. | Send exactly one. |
409 ResourceConflict | The externalId is already used on another of your invoices. | Look up the existing invoice with filter[externalId][eq]. |
409 EligibilityBlocked on send | The engagement's payment eligibility requirements aren't complete (detailCode: payments.EngagementNotPaymentsEligible). | See Requirements and eligibility. |
409 on DELETE | The invoice was already sent. | Void it with POST .../void. |
404 ResourceNotFound | The 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
- Collect payment to get the invoice paid.
- Invoice lifecycle for what happens after sending.
- Recurring invoices to bill this client on a schedule.
Updated 10 days ago