Payee account linking
How an invited contractor links your Payee record to their own Wingspan Account, how link requests work, and how a trusted platform links Accounts directly.
This page explains how a Payee you created becomes linked to the contractor's own Wingspan Account. It covers the link request that tracks the process, what the invited person does to accept, what you can do as the payer while it's pending, and the trusted-platform path for when you already control both Accounts.
What linking is
A Payee is your record. The contractor's Account is theirs. Linking connects the two: once linked, the Payee's payeeAccountId points at the contractor's Account, and the contractor sees you as a Payer in their own Wingspan view.
Linking is the contractor accepting you as a payer. It isn't an identity-creation step. The invited person either already has an Account and accepts, or signs up, creates or selects an Account, and accepts.
Linking changes very little on your side:
- It sets
Payee.payeeAccountIdand moves the link request toLinked. - It doesn't change
Payee.id. Engagements, requirements, payables, and payment history keep pointing at the same Payee. - It doesn't merge relationships. If two payers invite the same person, each payer has its own Payee and its own link request, and both can resolve to the same Account.
- It doesn't reroute a payment that's already in flight.
The link request
A PayerPayeeLinkRequest is the durable record of one linking attempt. Both sides can read it. At most one non-terminal link request exists for a Payee at a time.
| Field | What it means |
|---|---|
id | The link request ID. You'll use it to accept, reject, retarget, or cancel. |
subject | { "type": "Payee", "id": "<payeeId>" } for a Payee invitation. A Payer invitation uses type: Payer. |
status | Pending, Linked, or Rejected. |
target | Who the request is addressed to: { "type": "Email", "email": ... } or { "type": "Account", "accountId": ... }. Omitted once linked or rejected. |
accountId | The Account that accepted. Set on Linked. |
personId | The Person behind that Account's principal, when there is one. |
linkMethod | How it reached Linked: RecipientAccepted, PlatformAssociated, or MigrationImported. |
associationAuthority | The evidence you supplied for a trusted-platform link. Omitted for normal accepts. |
events | createdAt, pendingAt, linkedAt, rejectedAt. |
stateDiagram-v2
[*] --> Pending: invite or create link request
Pending --> Linked: recipient accepts, or payer associates
Pending --> Rejected: recipient rejects
Pending --> [*]: payer cancels (DELETE)
Linked --> [*]
Rejected --> [*]
Linked and Rejected are terminal. A Linked request is the permanent record of the binding and can't be deleted. To try again after a rejection, create a new link request.
The Payee's linkRequestStatus mirrors the current request's status. It's absent when no link request exists, which is normal for a Payee you haven't invited yet.
How the invited person accepts
If you use Wingspan's hosted onboarding, Wingspan runs these steps for the contractor. If you build your own onboarding screens, these are the calls your app makes on the contractor's behalf.
sequenceDiagram
participant P as Payer (your backend)
participant W as Wingspan
participant C as Contractor
P->>W: POST /v3/payments/payees/{payeeId}/invite
W->>C: Email with one-time link
C->>W: POST /v3/platform/sessions (sessionType Invite)
C->>W: GET /v3/payments/link-requests/current
C->>W: Sign up or sign in, create or select an Account
C->>W: POST /v3/payments/link-requests/{id}/accept
W-->>P: Payee.payeeAccountId is set, linkRequestStatus is Linked
1. Open the invitation
The email link carries a one-time invite token. Exchange it for a short-lived Person session with POST /v3/platform/sessions. Send the token as the bearer and name the Invite session type in the body:
curl -X POST https://api.wingspan.app/v3/platform/sessions \
-H "Authorization: Bearer $INVITE_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "sessionType": "Invite" }'The response is a normal Person session without a refresh token. Use its token as the bearer for the next steps.
2. Show who sent the invitation
GET /v3/payments/link-requests/current returns the link request bound to this session, plus what your landing page needs to render it. It never returns your Payee ID or other counterparty IDs.
// 200 OK
{
"linkRequest": {
"id": "0bmLRtEASNfDLcLxpUNkyw",
"status": "Pending",
"target": { "type": "Email", "email": "[email protected]" },
"events": { "createdAt": "2026-09-24T15:03:40Z", "pendingAt": "2026-09-24T15:03:40Z" }
},
"inviteContext": {
"counterpartyDisplayName": "Northwind Staffing",
"message": "Welcome to Northwind Staffing. Please finish setup before your first shift.",
"invitedEmail": "[email protected]",
"invitedEmailMapsToExistingPerson": false
}
}Use invitedEmailMapsToExistingPerson to choose between a sign-in screen and a sign-up screen. A 404 means the session isn't bound to an invitation. A 410 means the request is no longer Pending (already accepted, rejected, or replaced), so show an "expired link" state.
A person invited by several payers can list all their invitations with GET /v3/payments/link-requests/incoming.
3. Choose the Account to link
The contractor needs an Account they control. If they already have one, they pick it. If not, they create one with POST /v3/platform/accounts, which runs as the Person and doesn't accept X-Wingspan-Account.
The contractor doesn't have to finish tax information, identity verification, or payout setup before accepting. Those come after, driven by the requirements on your engagements.
4. Accept or reject
POST /v3/payments/link-requests/{linkRequestId}/accept:
curl -X POST https://api.wingspan.app/v3/payments/link-requests/0bmLRtEASNfDLcLxpUNkyw/accept \
-H "Authorization: Bearer $CONTRACTOR_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{ "acceptingAccountId": "qY0t19FmElxN3ur1EiW2Ov" }'// 200 OK (trimmed)
{
"id": "0bmLRtEASNfDLcLxpUNkyw",
"subject": { "type": "Payee", "id": "M9ISYbzJElXs4zIHv76rjT" },
"status": "Linked",
"accountId": "qY0t19FmElxN3ur1EiW2Ov",
"personId": "2MRBaewOMAak9Oyf9dFVBa",
"linkMethod": "RecipientAccepted",
"events": { "linkedAt": "2026-09-24T16:10:05Z" }
}The caller must match the request's target, or the accept returns 403.
To decline, call POST /v3/payments/link-requests/{linkRequestId}/reject with an optional {"reason": "..."}.
What you can do as the payer
| Task | Call |
|---|---|
| See the current and past link requests for a Payee | GET /v3/payments/payees/{payeeId}/link-requests |
| Read one link request | GET /v3/payments/link-requests/{linkRequestId} |
| Send a pending request to a different email (the old link stops working) | PATCH /v3/payments/link-requests/{linkRequestId} with {"targetEmail": "..."} |
| Withdraw a pending request | DELETE /v3/payments/link-requests/{linkRequestId} |
| Start a new request after a rejection, or target an existing Account | POST /v3/payments/payees/{payeeId}/link-requests with exactly one of targetEmail or targetWingspanAccountId |
Creating a new request while a non-terminal one exists returns 409. Retargeting or cancelling a Linked or Rejected request also returns 409.
Link directly as a trusted platform
Some platforms already know the contractor and control both Accounts: for example, a platform that operates its workers' Accounts inside its own Organization, or a migration of records whose consent was collected outside Wingspan. In that case you can skip the invitation and bind the Account yourself with POST /v3/payments/payees/{payeeId}/associate-account.
Your request must act as the payer Account (use X-Wingspan-Account if needed), and you must also have access to the contractor's Account, for example because both Accounts are in the same Organization or you hold an Authorization for both.
curl -X POST https://api.wingspan.app/v3/payments/payees/M9ISYbzJElXs4zIHv76rjT/associate-account \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "X-Wingspan-Account: z9uv0jPAxTqSLs5UKwv1DE" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"payeeAccountId": "qY0t19FmElxN3ur1EiW2Ov",
"authority": {
"type": "PlatformAsserted",
"basis": "ContractorAgreement",
"externalAgreementId": "AGR-2026-00931",
"acceptedAt": "2026-09-01T12:00:00Z",
"consentVersion": "2026.2"
}
}'The response is the PayerPayeeLinkRequest, now Linked with linkMethod: PlatformAssociated (or MigrationImported when authority.type is MigrationImported). The authority you send is stored for audit. You can attach signed evidence with authority.evidenceFileId, a file you uploaded to /v3/compliance/vault-files (see Files and documents).
- If the Payee is already linked to a different Account, you get
409 ResourceConflict. - If you can't act on
payeeAccountId, you get403 AccountMismatch. POST /v3/payments/payeesnever accepts an Account ID. Create the Payee first, then associate.
Association only records the link and your authority for it. It doesn't create an Account or Person, and it doesn't count as the contractor accepting any banking terms or consents that must be captured separately in onboarding.
Invite a payer to link
The same link request mechanism works in the other direction. If you bill clients, you keep a Payer record for each one and can invite the client to link their own Account with POST /v3/payments/payers/{payerId}/invite or POST /v3/payments/payers/{payerId}/link-requests. The client accepts with the same accept call. See Invoices overview.
Webhooks
PayerPayeeLinkRequest.* events aren't available for subscription yet. Poll the Payee's linkRequestStatus or its link request list until they are.
After associate-account, the Payee also records an Activated transition once its activation conditions are met, and that fires Payee.Activated, which you can subscribe to. See Payees.
Common mistakes
- Waiting for linking before doing anything else. You can place an unlinked Payee on an engagement and pay it. Linking isn't a prerequisite for contractor payments.
- Reopening a rejected request. You can't. Create a new link request.
- Using
associate-accountto skip consent you don't have. Only use it when your platform has already established the contractor's identity and consent, and record that basis inauthority. - Showing a generic error on an old link. A
410fromlink-requests/currentmeans the invitation was already used or replaced. Tell the contractor to sign in, or ask the payer to resend.
Related
Updated 10 days ago