Search
Query Wingspan V3 search rows for payees, payers, invoices, and payables with free-text search, rich filters, and totals.
This page shows you how to use the /v3/search endpoints to find payees, payers, invoices, and payables with free-text search and filters that the regular list endpoints don't offer. They're the V3 version of the V1 /search/*-row endpoints and return the same kind of rows.
Search rows compared with list endpoints
The search endpoints read from a separate search index. Each row is a flattened copy of a payee, payer, invoice, or payable, built for filtering and display. That has two consequences:
- Rows are eventually consistent. A change you just made can take a few seconds or more to show up in search. Use search to find records and fill lists and dashboards. Don't use it to confirm that a write happened.
- Rows aren't the record of truth. Each row's
idis the ID of the underlying resource. When you need the current, complete record, read it from its regular endpoint, such asGET /v3/payments/payees/{payeeId}orGET /v3/payments/payables/{payableId}.
The search endpoints are read-only. Create and change records through their regular endpoints.
The four endpoints
| Endpoint | Whose view | What each row is |
|---|---|---|
GET /v3/search/payee-rows | Payer | Someone you pay |
GET /v3/search/payer-rows | Payee | Someone who pays you |
GET /v3/search/invoice-rows | Payee | An invoice you issued |
GET /v3/search/payable-rows | Payer | A payable you owe |
V1 served both sides of invoices from one /search/invoice-row endpoint. In V3, pick invoice-rows for the payee's side or payable-rows for the payer's side.
Search by text
filter[searchString][eq] runs a free-text search across the row's name and contact fields. It needs 2 to 256 characters:
curl -g "https://api.wingspan.app/v3/search/payee-rows?filter[searchString][eq]=priya&page[size]=25" \
-H "Authorization: Bearer $WINGSPAN_TOKEN"With a search string, results are ordered by relevance first. Without one, payee rows are ordered newest first, and invoice and payable rows by due date, latest first.
Filter
Search uses the same filter[field][operator]=value syntax as the rest of V3. See Filtering, sorting, and expanding. Each endpoint's reference page lists its fields. A few useful ones on payee rows:
| Filter | Finds |
|---|---|
filter[id][anyOf][]=... | Specific payees by Payee.id, for refreshing a set of rows at once |
filter[status][eq]=Activated | Payees in a given status |
filter[externalId][eq]=... | A payee by your own ID |
filter[engagements.engagementId][eq]=... | Payees on a given engagement |
filter[batchIds][eq]=... | Payees created or updated by a given batch |
filter[customFields.{key}][eq]=... | Payees with a given custom field value |
filter[metadata.{key}][eq]=... | Payees with a given metadata value |
For lists of values, search accepts only anyOf and noneOf; any other spelling returns 422 ValidationError. On customFields.{key} and metadata.{key} filters you can use eq, ne, anyOf, noneOf, and contains. Values must be non-empty strings, so send booleans as "true" or "false". Matching on these keys is by analyzed terms and isn't case-sensitive: every term in the value must appear. A custom field key that doesn't exist matches no rows.
Invoice and payable rows can filter on date and timestamp ranges with gt, gte, lt, and lte, for example filter[dueDate][lte]=2026-04-30.
Cancelled invoices and payables are included. To hide them, add filter[status][noneOf][]=Cancelled.
Invoice rows include internal payment records that aren't customer invoices, marked with metadata.invoiceType of approvedInvoicesPayment. To leave them out, add filter[metadata.invoiceType][ne]=approvedInvoicesPayment.
Sort
Search endpoints accept exactly one sort field, for example sort[createdAt]=desc. Payee and payer rows sort on createdAt or updatedAt. Invoice and payable rows can also sort on fields such as dueDate and paidAt. The reference page lists each endpoint's sort fields. With a search string, your sort becomes the tiebreaker after relevance.
Pages and totals
Search uses the same page[size] and page[token] parameters as other lists. See Pagination. Two details differ:
- Tokens belong to one exact query. A search
page[token]works only with the exact filters and sort that produced it, and not with any other endpoint. pagination.totalSizeis the exact number of matching rows.
Freeze the results into a snapshot
Search rows change while you page through them, so a long walk can skip or repeat rows. To page through a fixed set instead, add page[mode]=materialized to the first request. Wingspan saves the filtered, sorted result set as a snapshot and returns its ID:
curl -g "https://api.wingspan.app/v3/search/payable-rows?filter[status][eq]=Opened&page[size]=100&page[mode]=materialized" \
-H "Authorization: Bearer $WINGSPAN_TOKEN"// (trimmed)
{
"data": [ { "id": "Pb2xK7mQ4tLw9nRv1cZd8s", "status": "Opened" } ],
"pagination": {
"nextPageToken": "eyJvIjoxMDB9",
"totalSize": 5230,
"resultSetId": "Rs6hTq1VnK8pXw3cMd5bLa",
"resultSetSize": 5230
}
}For every later page, send page[resultSetId] with the nextPageToken you got back:
curl -g "https://api.wingspan.app/v3/search/payable-rows?page[resultSetId]=Rs6hTq1VnK8pXw3cMd5bLa&page[token]=eyJvIjoxMDB9&page[size]=100" \
-H "Authorization: Bearer $WINGSPAN_TOKEN"- The filters and sort are fixed when the snapshot is made. Snapshot pages don't apply them again, so you don't need to resend them.
totalSizeandresultSetSizeare the snapshot's size.- A snapshot holds at most 100,000 rows; a larger match returns
422 ValidationError. Narrow the filters. - A malformed page token on a snapshot page returns
422 ValidationError. - A snapshot expires. An expired
resultSetIdreturns404withcode: ResourceNotFound. Start again withpage[mode]=materializedto make a new one.
Use snapshots for exports and bulk jobs. For an ordinary list screen, the default mode is enough.
Invoice and payable row responses also include a summary with the count and amountTotal of every matching row, not only the current page. It's useful for "42 invoices, $18,300.00" displays.
Which Account you search
A search always runs inside one Account, the Account your request is acting on. To search a child Account, send X-Wingspan-Account. There's no way to search across several Accounts in one request. See Acting on behalf of Accounts.
Engagements on a row
Payee and payer rows carry an engagements list with a short summary of each engagement: its ID, status, and, when the engagement's template declared one, engagementType (Contractor, Employee, EmployeeOfRecord, or AgentOfRecord). A missing engagementType means the type is unknown. Don't read it as Contractor.
On payer rows, the list covers both directions of the relationship: engagements the payer created for you, and engagements you created for that payer (such as the assignment an invoice is billed against). filter[engagements.id][eq] matches either kind. To read the full engagement, use GET /v3/payments/payers/{payerId}/engagements for the ones you created.
Search rows never include tax identifiers. Read the payee's tax information through its regular endpoints if you're allowed to see it.
From V1 search filters
V1 /search/*-row filter | V3 filter |
|---|---|
searchString | filter[searchString][eq] |
field=value | filter[field][eq]=value |
field!=value | filter[field][noneOf][]=value |
field in [...] | filter[field][anyOf][]=... repeated |
payerOwnedData.status | filter[status][eq] |
payerOwnedData.payeeExternalId | filter[externalId][eq] |
payeeId (payee rows) | filter[payeeAccountId][eq] |
payerId (payer rows) | filter[payerAccountId][eq] |
labels.bulkBatchId | filter[batchIds][eq] |
payerOwnedData.customFields.{key} | filter[customFields.{key}][eq] |
clientId (invoice rows) | filter[payerAccountId][eq] on invoice-rows |
memberId (invoice rows) | filter[payeeAccountId][eq] on payable-rows |
client.workflowStatus | filter[payerApprovalStatus][eq] |
invoiceTemplateId | filter[recurringInvoiceId][eq] or filter[recurringPayableId][eq] |
Cancelled invoice rows left out by default | Included. Add filter[status][noneOf][]=Cancelled to leave them out |
Sort by internal.events.paymentDueAt | sort[scheduledPaymentDate] on payable-rows, which can now also be filtered |
In V3, payeeAccountId and payerAccountId are Account IDs, and id is the relationship record's ID. Don't mix them up when you refresh rows by ID.
Related pages
Updated 10 days ago