Request and response conventions
How Wingspan V3 requests and responses are shaped, including IDs, casing, money, dates, metadata, externalId, events, actors, and updates.
Use the shared Wingspan V3 conventions to read and write resources consistently. The API reference lists the exact fields for each operation.
URLs
Every request goes to https://api.wingspan.app and every path starts with /v3/{domain}/:
| Domain | What lives there |
|---|---|
/v3/payments | Payees, payers, engagements, invoices, payables, payroll, payouts, and other money movement |
/v3/finance | Bank accounts, transfers, cards, transactions, and tax withholding |
/v3/onboarding | Identity verification, requirements, and reference data |
/v3/compliance | Tax forms and filings, insurance, and vault files |
/v3/platform | Accounts, persons, sessions, webhooks, events, batches, custom fields, and async operations |
/v3/search is a separate read-only surface for search rows. See Search.
A few more rules make paths predictable:
- Path segments are
kebab-case(/v3/compliance/vault-files) and path parameters arecamelCase({payeeId}). - Your own ID never appears in the URL. Wingspan knows who you are from your token. There is no
/meendpoint. - To work inside a child or authorized Account, send
X-Wingspan-Account. See Acting on behalf of Accounts.
Headers you'll use
| Header | When |
|---|---|
Authorization: Bearer <token> | Every authenticated request |
Content-Type: application/json | Every request with a body |
Idempotency-Key | Creates, money movement, and any POST or PATCH you might retry. Required on some operations. See Idempotency |
If-Match | Updates and state changes whose reference page lists it. See Concurrency and ETags |
X-Wingspan-Account | Acting in another Account. See Acting on behalf of Accounts |
Successful responses are application/json. Errors are application/problem+json. See Errors.
IDs
Wingspan IDs are opaque strings. They have no type prefix and they are not UUIDs. Most are 22 characters, for example Qm7tR2vXk9LpZ4wNc8HsYa. Some records created before V3 use other shapes:
- Older 22-character IDs can contain a
.. - Some Accounts migrated from V1 retain a 24-character lowercase hexadecimal ID.
- Some mandates have a 32-character hexadecimal ID.
- Some records carried over from V1 and V2 have composite IDs of 22 to 80 characters.
Store IDs as strings that support up to 80 characters, and do not validate their shape.
Foreign keys are named after the resource they point to: payeeId, payerId, payeeEngagementId. References to vault files use fileId (or a name ending in FileId, such as attachmentFileId).
Casing
- Field names are
camelCase:externalId,dueDate,payeeAccountId. - Enum values are
TitleCase:Activated,PaymentInTransit,PartiallyPaid.
Some values retain the spelling of an outside standard. Sort directions are asc and desc, OAuth values follow the OAuth specifications, and tax form types start with a digit (1099Nec).
The allowed expand values are case-sensitive and listed for each endpoint. See Filtering, sorting, and expanding.
Dates and times
- Timestamps are ISO 8601 in UTC with a
Z:"2026-03-27T10:30:00Z". - Calendar dates with no time are
YYYY-MM-DD:"2026-04-15".
Money
Amounts are JSON numbers in major units, such as 1250.00 for $1,250. They are never strings or integer cents. Set the currency once on the document (the invoice, payable, or payroll run) in a currency field such as "USD". Every amount on that document uses that currency.
The API rejects an amount sent as a string, or with more decimal places than the currency allows, with 422 ValidationError.
Fields most resources share
Resources that you create and query carry a common set of fields:
| Field | What it holds |
|---|---|
id | The Wingspan ID. |
status | The lifecycle state, on resources that have one. |
events | Timestamps for each transition, each named {transition}At: createdAt, openedAt, paidAt. A missing key means the transition hasn't happened. |
actors | Who made each transition, each named {transition}By: createdBy, paidBy. The value is the ID of the Person or ServiceAccount, or SYSTEM when Wingspan made the change itself. |
metadata | Your own key-value strings. Up to 50 keys, keys up to 40 characters, values up to 500 characters. |
externalId | Your own ID for the record, on resources you'd reconcile with another system. |
Here is a trimmed payable as an example:
// (trimmed)
{
"id": "b3VhT6gJq1MzR8xKd5WnPe",
"externalId": "INV-2026-0412",
"status": "Opened",
"currency": "USD",
"amount": 1250.00,
"dueDate": "2026-04-15",
"events": {
"createdAt": "2026-04-01T14:02:11Z",
"openedAt": "2026-04-01T14:05:40Z"
},
"actors": {
"createdBy": "Hn4dW8kPq2ZxM7vRt5LcJa",
"openedBy": "Hn4dW8kPq2ZxM7vRt5LcJa"
},
"metadata": {
"costCenter": "east-region"
}
}The events timestamps line up with webhook event names. For example, events.paidAt on an invoice corresponds to the Invoice.Paid event. Not every transition has a subscribable event yet. Event types lists the ones you can subscribe to.
externalId
Use externalId to store the ID your own system uses for a record. Every read returns it. List endpoints that support it accept filter[externalId][eq]=..., so you can look up a record by your ID.
externalId is unique per Account and resource type. Creating a second record of the same type with the same externalId returns 409 ResourceConflict. Create is never an upsert, so to recover, look the existing record up by externalId and update it instead.
externalId is a real field. Don't put your IDs in metadata when the resource has an externalId.
metadata
metadata holds strings only. It's for your own tags and references, not for data Wingspan acts on. A few list endpoints can filter on metadata keys, and their reference pages say so.
State changes use action endpoints
To move a resource through its lifecycle, call a verb on it: POST /v3/payments/payables/{payableId}/open, POST /v3/platform/batches/{batchId}/process. You never change status with a PATCH.
This keeps each transition explicit. The verb determines the resulting status, the events.*At timestamp, and the webhook event name. For example, open returns status: Opened and adds events.openedAt.
An action returns 200 with the updated resource. It returns 409 InvalidStateTransition when the resource is not in an allowed state.
Updating a resource
Send only the fields you want to change in a PATCH body. There is no update mask.
- A field you send with a value replaces the stored value.
- A field you leave out stays as it is.
- To clear a field, send
nullif the reference marks the field nullable. A few string fields, such as a batch'sname, clear with an empty string instead; the field description says so. If neither is documented, the field can't be cleared. - How
metadataupdates depends on the operation. Some merge the keys you send into the stored map, and others replace the whole map. The field description in the reference says which.
Some endpoints reject fields they don't recognize with 422 ValidationError. Others ignore them. Check field names against the reference: a misspelled field on an endpoint that ignores it leaves the stored value unchanged.
Status codes
| Code | Meaning |
|---|---|
200 | A read, an update, or an action that returns data |
201 | A resource was created |
202 | Long-running work was accepted. See Async operations |
204 | A delete succeeded. There's no body. |
302 | A file download redirect. See Files and documents |
Error codes are listed in Errors.
Every GET also answers HEAD with the same status and headers and no body.
Related pages
Updated 10 days ago