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.payeeAccountId and moves the link request to Linked.
  • 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.

FieldWhat it means
idThe 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.
statusPending, Linked, or Rejected.
targetWho the request is addressed to: { "type": "Email", "email": ... } or { "type": "Account", "accountId": ... }. Omitted once linked or rejected.
accountIdThe Account that accepted. Set on Linked.
personIdThe Person behind that Account's principal, when there is one.
linkMethodHow it reached Linked: RecipientAccepted, PlatformAssociated, or MigrationImported.
associationAuthorityThe evidence you supplied for a trusted-platform link. Omitted for normal accepts.
eventscreatedAt, 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

TaskCall
See the current and past link requests for a PayeeGET /v3/payments/payees/{payeeId}/link-requests
Read one link requestGET /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 requestDELETE /v3/payments/link-requests/{linkRequestId}
Start a new request after a rejection, or target an existing AccountPOST /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 get 403 AccountMismatch.
  • POST /v3/payments/payees never 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-account to 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 in authority.
  • Showing a generic error on an old link. A 410 from link-requests/current means the invitation was already used or replaced. Tell the contractor to sign in, or ask the payer to resend.

Related


Did this page help you?