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.

ResourceWho owns itWhat it is
PayeeThe payer's AccountThe payer's record of someone it pays.
PayerThe payee's AccountThe same relationship seen from the payee's side.
AccountThe contractor or companyA business identity in Wingspan. A contractor's own Account is tied to their login.
PersonNobody (a human)A person who can sign in.
PayerPayeeLinkRequestBoth sides can read itThe 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

FieldWhat it means
idThe Payee's ID. Use it everywhere; it stays the same before and after linking.
accountIdYour Account (the owner of this record). Read-only.
payeeAccountIdThe contractor's own Account. Absent until the contractor accepts your invitation or you associate an Account.
payerIdThe ID of the matching Payer record on the payee's side. It may equal id.
emailThe invitation address. Required on create.
contextContractor 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.
externalIdYour own ID for this Payee (from your ERP, ATS, or HRIS). Unique per Account. Filterable.
statusActivated or Deactivated. An administrative flag only (see below).
linkRequestStatusPending, Linked, or Rejected. Linking progress, read-only. Absent when no link request exists.
profileDisplay information: displayName and doingBusinessAs. Stored and returned exactly as you sent them. Not the legal or tax identity.
payerSuppliedComplianceEntityIdA tax identity you supplied for this Payee before it linked. See Tax information.
identitySourcePayerProvided or PayeeOwned. Whose tax identity is attached. Read-only.
complianceEntitySourceStrategyAutomatic (default), PayerProvided, or PayeeProvided. Whose ComplianceEntity a read with ?expand=ComplianceEntity shows. See Tax information.
defaults.isAutoPayEnabledYour 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.verificationStrategyAll or None. Whether tax-information verification checks are required.
w9SourcePolicy, payoutDestinationPolicyWhose data Wingspan uses for the W-9 and for the payout destination: PayerSupplied, PayeeSupplied, or PayeeSuppliedFallbackToPayer (the default for new Payees).
internalNotesNotes only your team can see.
events, actorscreatedAt, updatedAt, activatedAt, deactivatedAt, contextChangedAt, and who did each.
metadataUp 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.

StatusMeaningHow you get there
ActivatedThe relationship is active. Every new Payee starts here.Create, or POST .../activate to reactivate.
DeactivatedYou'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 linkRequestStatus and payeeAccountId.
  • Can I pay them through this engagement? Read paymentsEligibility on 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 externalId is already used by another Payee in your Account, you get 409 with code: ResourceConflict. Look up the existing Payee with GET /v3/payments/payees?filter[externalId][eq]=ATS-04821.
  • If email matches an existing Payee, one of two things happens, and you can't predict which from the API. Handle both:
    • 409 ResourceConflict with detailCode: payments.PayeeEmailConflict. Find the existing Payee with filter[email][eq] and PATCH it.
    • 201 with the existing Payee (reactivated if it was Deactivated). Nothing you sent is applied, and events.createdAt shows 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 201 doesn't always mean a new Payee was created. Compare events.createdAt with 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 Created or Activated. You get 409, so terminate or delete those engagements first.
  • A Suspended engagement doesn't block the change, but you can't resume it afterward.
  • A Deactivated Payee returns 409. 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:

  1. POST /v3/payments/payees/{payeeId}/update-email/preview with {"targetEmail": "..."}. Nothing is written. The response tells you what would happen: resultType is UpdatedEmail, NewPayee, or ExistingPayee, and it lists the engagement, invoice, and deduction IDs that would move plus any conflictFields.
  2. POST /v3/payments/payees/{payeeId}/update-email with the same body applies it. The response's targetPayeeId is 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: Activated as "ready to pay." Every Payee is Activated the moment you create it. Check paymentsEligibility on the PayeeEngagement instead.
  • Assuming a 201 means a new record. A matching email can return the existing Payee. Check events.createdAt.
  • Putting your system's ID in metadata. Use externalId. 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/payees doesn't accept one. If you already control the contractor's Account, create the Payee and then call associate-account. See Payee account linking.
  • Trying to PATCH context. Use change-context, after ending the engagements of the current context.
  • Creating a second Payee when the contractor changes email. Use the update-email flow so their engagements and payment history stay together.

Related


Did this page help you?