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 id is the ID of the underlying resource. When you need the current, complete record, read it from its regular endpoint, such as GET /v3/payments/payees/{payeeId} or GET /v3/payments/payables/{payableId}.

The search endpoints are read-only. Create and change records through their regular endpoints.

The four endpoints

EndpointWhose viewWhat each row is
GET /v3/search/payee-rowsPayerSomeone you pay
GET /v3/search/payer-rowsPayeeSomeone who pays you
GET /v3/search/invoice-rowsPayeeAn invoice you issued
GET /v3/search/payable-rowsPayerA 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:

FilterFinds
filter[id][anyOf][]=...Specific payees by Payee.id, for refreshing a set of rows at once
filter[status][eq]=ActivatedPayees 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.totalSize is 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.
  • totalSize and resultSetSize are 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 resultSetId returns 404 with code: ResourceNotFound. Start again with page[mode]=materialized to 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 filterV3 filter
searchStringfilter[searchString][eq]
field=valuefilter[field][eq]=value
field!=valuefilter[field][noneOf][]=value
field in [...]filter[field][anyOf][]=... repeated
payerOwnedData.statusfilter[status][eq]
payerOwnedData.payeeExternalIdfilter[externalId][eq]
payeeId (payee rows)filter[payeeAccountId][eq]
payerId (payer rows)filter[payerAccountId][eq]
labels.bulkBatchIdfilter[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.workflowStatusfilter[payerApprovalStatus][eq]
invoiceTemplateIdfilter[recurringInvoiceId][eq] or filter[recurringPayableId][eq]
Cancelled invoice rows left out by defaultIncluded. Add filter[status][noneOf][]=Cancelled to leave them out
Sort by internal.events.paymentDueAtsort[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


Did this page help you?