Payees
How a Payee works in the V3 API, why it doesn't need a linked Account to be paid, and how to create, update, and deactivate one.
A Payee is your record of a contractor or vendor you pay. This page explains what a Payee holds, how it relates to the payee's own Wingspan Account, and how to create, update, and deactivate one. Payees were called collaborators in the V1 API.
How a Payee fits with other resources
A Payee belongs to you, the payer. It's the durable record of one relationship: "Northwind Staffing pays Priya Shah." Everything you do with that person points at the Payee's id: engagements, requirements, payables, payout routing, and payment history.
| Resource | Who owns it | What it is |
|---|---|---|
Payee | The payer's Account | The payer's record of someone it pays. |
Payer | The payee's Account | The same relationship seen from the payee's side. |
Account | The contractor or company | A business identity in Wingspan. A contractor's own Account is tied to their login. |
Person | Nobody (a human) | A person who can sign in. |
PayerPayeeLinkRequest | Both sides can read it | The record that links a Payee to the payee's own Account. |
A Payee doesn't need a linked Account. You can create a Payee, put it on an engagement, and pay it before the contractor ever signs in. When the contractor accepts your invitation, Wingspan sets payeeAccountId on the Payee. The Payee's id never changes, so every engagement, payable, and payment you created before linking still points at the same record.
Key fields
| Field | What it means |
|---|---|
id | The Payee's ID. Use it everywhere; it stays the same before and after linking. |
accountId | Your Account (the owner of this record). Read-only. |
payeeAccountId | The contractor's own Account. Absent until the contractor accepts your invitation or you associate an Account. |
payerId | The ID of the matching Payer record on the payee's side. It may equal id. |
email | The invitation address. Required on create. |
context | Contractor or Employee: the kind of worker this Payee is to you. Required on create. It decides which engagement types the Payee can hold. See Worker context. |
externalId | Your own ID for this Payee (from your ERP, ATS, or HRIS). Unique per Account. Filterable. |
status | Activated or Deactivated. An administrative flag only (see below). |
linkRequestStatus | Pending, Linked, or Rejected. Linking progress, read-only. Absent when no link request exists. |
profile | Display information: displayName and doingBusinessAs. Stored and returned exactly as you sent them. Not the legal or tax identity. |
payerSuppliedComplianceEntityId | A tax identity you supplied for this Payee before it linked. See Tax information. |
identitySource | PayerProvided or PayeeOwned. Whose tax identity is attached. Read-only. |
complianceEntitySourceStrategy | Automatic (default), PayerProvided, or PayeeProvided. Whose ComplianceEntity a read with ?expand=ComplianceEntity shows. See Tax information. |
defaults.isAutoPayEnabled | Your accounts-payable auto-pay default for this Payee. PayeeEngagements can override it with overrides.isAutoPayEnabled. This is separate from automatic collection of invoices a payee sends a client, which lives on the payee's Payer record (isAutomaticInvoiceCollectionEnabled). See Collect payment. |
defaults.verificationStrategy | All or None. Whether tax-information verification checks are required. |
w9SourcePolicy, payoutDestinationPolicy | Whose data Wingspan uses for the W-9 and for the payout destination: PayerSupplied, PayeeSupplied, or PayeeSuppliedFallbackToPayer (the default for new Payees). |
internalNotes | Notes only your team can see. |
events, actors | createdAt, updatedAt, activatedAt, deactivatedAt, contextChangedAt, and who did each. |
metadata | Up to 50 keys of your own data. |
Legal names, tax IDs, and addresses don't live on the Payee. They live on a ComplianceEntity, so identity data has one home and one verification history.
Status
status tells you whether the relationship is active in your account. It doesn't tell you whether the Payee accepted the invitation, and it doesn't tell you whether they can be paid.
| Status | Meaning | How you get there |
|---|---|---|
Activated | The relationship is active. Every new Payee starts here. | Create, or POST .../activate to reactivate. |
Deactivated | You've stopped working with this Payee. Future payments are blocked. The record stays readable. | DELETE /v3/payments/payees/{payeeId} |
stateDiagram-v2
[*] --> Activated: create
Activated --> Deactivated: DELETE
Deactivated --> Activated: POST /activate
Three separate things answer three separate questions:
- Is the relationship active? Read
status. - Has the contractor linked their Account? Read
linkRequestStatusandpayeeAccountId. - Can I pay them through this engagement? Read
paymentsEligibilityon the PayeeEngagement. See Requirements and eligibility.
Create a payee
POST /v3/payments/payees creates the record. It doesn't send an invitation, and it doesn't create an Account or Person. To invite, call POST /v3/payments/payees/{payeeId}/invite afterward (see Invite a payee).
curl -X POST https://api.wingspan.app/v3/payments/payees \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"email": "[email protected]",
"externalId": "ATS-04821",
"context": "Contractor",
"profile": { "displayName": "Priya Shah" },
"metadata": { "region": "west" }
}'// 201 Created (trimmed)
{
"id": "M9ISYbzJElXs4zIHv76rjT",
"accountId": "z9uv0jPAxTqSLs5UKwv1DE",
"email": "[email protected]",
"externalId": "ATS-04821",
"status": "Activated",
"context": "Contractor",
"profile": { "displayName": "Priya Shah" },
"events": { "createdAt": "2026-09-24T15:02:11Z" },
"metadata": { "region": "west" }
}email and context are required. There's no linkRequestStatus and no payeeAccountId yet. Both appear once you invite the Payee.
Duplicates: externalId and email
Create is never an upsert.
- If
externalIdis already used by another Payee in your Account, you get409withcode: ResourceConflict. Look up the existing Payee withGET /v3/payments/payees?filter[externalId][eq]=ATS-04821. - If
emailmatches an existing Payee, one of two things happens, and you can't predict which from the API. Handle both:409 ResourceConflictwithdetailCode: payments.PayeeEmailConflict. Find the existing Payee withfilter[email][eq]andPATCHit.201with the existing Payee (reactivated if it wasDeactivated). Nothing you sent is applied, andevents.createdAtshows the original creation time.
If the existing Payee's context differs from the one you sent, you get 409 ResourceConflict instead of the 201. Change the context with change-context (below), not by creating again.
Important: A
201doesn't always mean a new Payee was created. Compareevents.createdAtwith the time of your request, or look the Payee up by email first, before assuming the fields you sent were saved.
An email held only by a Deactivated Payee doesn't conflict; that create proceeds normally.
List and find payees
GET /v3/payments/payees supports these filters: status (eq, anyOf), linkRequestStatus (eq, anyOf), context (eq, anyOf), externalId (eq), and email (eq). A Contractor context filter also returns Payees created before context existed, which read as Contractor.
# Payees who were invited but haven't accepted yet
curl -G https://api.wingspan.app/v3/payments/payees \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
--data-urlencode "filter[linkRequestStatus][eq]=Pending" \
--data-urlencode "page[size]=50"Keep calling with page[token] set to pagination.nextPageToken until it's an empty string. See Pagination.
GET /v3/payments/payees/{payeeId} returns one Payee, including branding resolved from the linked Account when there is one. Add ?expand=ComplianceEntity to include a summary of the tax identity selected by complianceEntitySourceStrategy as complianceEntity, with complianceEntitySource saying whose it is. The summary has names, legal form, business details, phone, and addresses. It never includes tax IDs, dates of birth, or verification results. Any other expand value returns 422.
For full-text search across payees, use /v3/search/payee-rows.
Update a payee
PATCH /v3/payments/payees/{payeeId} changes only the fields you send. You can update profile, phone, externalId, defaults, internalNotes, complianceEntitySourceStrategy, w9SourcePolicy, payoutDestinationPolicy, and metadata. Send an empty string to clear externalId or internalNotes. profileSource was removed, and a request that sends it is rejected.
curl -X PATCH https://api.wingspan.app/v3/payments/payees/M9ISYbzJElXs4zIHv76rjT \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"internalNotes": "Prefers text messages for shift updates.",
"defaults": { "isAutoPayEnabled": true }
}'email and context aren't editable through PATCH. See the next two sections.
Worker context
context records the kind of worker a Payee is to you: Contractor (a 1099 contractor or vendor) or Employee (a W-2 employee). A Payee has exactly one context at a time, and it decides which engagement types the Payee can hold. Only an Employee engagement fits the Employee context. The other engagement types fit Contractor. A mismatched PayeeEngagement returns 409 ResourceConflict. See Engagements.
Payees created before context existed read as Contractor until their first engagement records the context that engagement implies.
To change it, call POST /v3/payments/payees/{payeeId}/change-context with the new value. It works in both directions and doesn't change status.
curl -X POST https://api.wingspan.app/v3/payments/payees/M9ISYbzJElXs4zIHv76rjT/change-context \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "context": "Employee" }'The response is 200 with the updated Payee, and events.contextChangedAt records when it changed.
- You can't change context while any engagement of the current context is
CreatedorActivated. You get409, so terminate or delete those engagements first. - A
Suspendedengagement doesn't block the change, but you can't resume it afterward. - A
DeactivatedPayee returns409. Activate it first. - Sending the context the Payee already has changes nothing.
There's no webhook for a context change.
Change a payee's email
Changing the email can move records between Payees, so it's a two-step flow:
POST /v3/payments/payees/{payeeId}/update-email/previewwith{"targetEmail": "..."}. Nothing is written. The response tells you what would happen:resultTypeisUpdatedEmail,NewPayee, orExistingPayee, and it lists the engagement, invoice, and deduction IDs that would move plus anyconflictFields.POST /v3/payments/payees/{payeeId}/update-emailwith the same body applies it. The response'stargetPayeeIdis the Payee to use afterward.
A Payee created in V3 and not yet linked keeps its id. A Payee that came from V1 may be replaced or merged into another record, so always read targetPayeeId from the response.
Deactivate and reactivate
DELETE /v3/payments/payees/{payeeId} sets status to Deactivated and returns 204. It doesn't erase the record or its history.
POST /v3/payments/payees/{payeeId}/activate sets it back to Activated.
We use a POST verb for the state change instead of letting you PATCH the status field, so the audit trail always records which action happened and who took it.
Payee activity history
GET /v3/payments/payees/{payeeId}/events returns the audit history for a Payee, newest first. It's paginated like any list.
Webhooks
You can subscribe to Payee.Activated. It fires when a Payee records an Activated transition, the same transition that sets events.activatedAt. For example, when a trusted platform links the contractor's Account with associate-account, Payee.Activated follows once the Payee's activation conditions are met. The payload identifies the Payee, so read it with GET /v3/payments/payees/{payeeId} for details. Don't rely on it as an invitation-accepted signal. Read linkRequestStatus for that.
Other Payee events (Payee.Created, Payee.Invited, Payee.Deactivated, and others) aren't available for subscription yet. For those, poll GET /v3/payments/payees with a linkRequestStatus or status filter. See Event types for what's currently subscribable.
Common mistakes
- Treating
status: Activatedas "ready to pay." Every Payee isActivatedthe moment you create it. CheckpaymentsEligibilityon the PayeeEngagement instead. - Assuming a
201means a new record. A matching email can return the existing Payee. Checkevents.createdAt. - Putting your system's ID in
metadata. UseexternalId. It's echoed on every read, it's unique per Account, and you can filter by it. - Passing the contractor's Account ID on create.
POST /v3/payments/payeesdoesn't accept one. If you already control the contractor's Account, create the Payee and then callassociate-account. See Payee account linking. - Trying to
PATCHcontext. Usechange-context, after ending the engagements of the current context. - Creating a second Payee when the contractor changes email. Use the
update-emailflow so their engagements and payment history stay together.
Related
Updated 10 days ago