Add and assign employees

Add an employee as a Payee, link their Wingspan account, and assign them to an Employee engagement with a worksite, start date, and job title in the V3 API.

An employee is a Payee with context: Employee. You pay them once they're assigned to an Employee engagement, which creates a PayeeEngagement that carries their worksite, start date, job title, tax elections, and the requirements they still need to complete. This page shows you how to add an employee, link their account, and assign them.

Set up W-2 payroll first: you need W-2 payroll turned on, a worksite, and an Employee engagement. See Set up W-2 payroll.

Add the employee

Create the employee with POST /v3/payments/payees. context and profile.email are required. Look the person up by externalId first, so a retry or a second import doesn't create a duplicate.

curl -G https://api.wingspan.app/v3/payments/payees \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  --data-urlencode "filter[externalId][eq]=emp-1042"
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 '{
    "context": "Employee",
    "externalId": "emp-1042",
    "profile": {
      "email": "[email protected]",
      "displayName": "Jordan Lee"
    }
  }'
// 201 Created (trimmed)
{
  "id": "Kzp8xceXy69Utto3wnouyQ",
  "payerId": "23Gvh0YtpH6bNj4iOxLqET",
  "externalId": "emp-1042",
  "status": "Activated",
  "context": "Employee",
  "profile": {
    "displayName": "Jordan Lee",
    "email": "[email protected]"
  }
}

context decides which engagement types the payee can hold. An Employee payee can only be assigned to Employee engagements. The email goes inside profile, not at the top level of the request.

Link the employee's account

Each employee has their own Wingspan account. Their tax profile and pay statements belong to it, so link it before you approve their first work log. You can link it in one of two ways. Payee account linking covers both in detail.

WayWhen to use it
The employee accepts an inviteThe employee signs in to Wingspan, or to your app through embedded onboarding, and finishes setup themselves.
You create the account and link itYou run the hire in your own system and already have the employee's consent from your offer letter or onboarding terms.

The employee accepts an invite

Send the invitation with POST /v3/payments/payees/{payeeId}/invite. The payee shows linkRequestStatus: Pending, and has no payeeAccountId, until the employee accepts and creates their login.

curl -X POST https://api.wingspan.app/v3/payments/payees/Kzp8xceXy69Utto3wnouyQ/invite \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{}'
// 200 OK (trimmed)
{
  "id": "Kzp8xceXy69Utto3wnouyQ",
  "status": "Activated",
  "linkRequestStatus": "Pending"
}

You can assign an invited employee before they accept. Their tax elections return 404 until they do, and approving their work log returns 422 payments.WorkLogPayrollProfileMissing.

You create the account and link it

Create the employee's account as a child of yours with POST /v3/platform/accounts. Make this call with a session that isn't bound to an account, and don't send X-Wingspan-Account. If your session is bound to an account, unbind it first with PATCH /v3/platform/sessions/{sessionId} and the body { "accountScope": null }, then use the token it returns. An API key owned by your account doesn't need this step.

curl -X POST https://api.wingspan.app/v3/platform/accounts \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "parentAccountId": "23Gvh0YtpH6bNj4iOxLqET",
    "externalId": "emp-1042-account"
  }'
// 201 Created (trimmed)
{
  "id": "IJoFzShicUxKFl03UuRGSd",
  "externalId": "emp-1042-account",
  "status": "Active",
  "parentAccountId": "23Gvh0YtpH6bNj4iOxLqET"
}

Then link the new account to the payee with POST /v3/payments/payees/{payeeId}/associate-account. authority is your record of why you were allowed to link it, such as the offer letter the employee accepted.

curl -X POST https://api.wingspan.app/v3/payments/payees/Kzp8xceXy69Utto3wnouyQ/associate-account \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "payeeAccountId": "IJoFzShicUxKFl03UuRGSd",
    "authority": {
      "type": "PlatformAsserted",
      "basis": "PlatformTerms",
      "externalAgreementId": "offer-letter-1042",
      "acceptedAt": "2026-11-02T14:00:00Z",
      "consentVersion": "v1"
    }
  }'
// 200 OK (trimmed)
{
  "id": "0WpT8OWnhwjrDs0JygvhG1",
  "subject": { "type": "Payee", "id": "Kzp8xceXy69Utto3wnouyQ" },
  "status": "Linked",
  "linkMethod": "PlatformAssociated",
  "accountId": "IJoFzShicUxKFl03UuRGSd"
}

Assign the employee to an Employee engagement

Assign the employee with POST /v3/payments/payees/{payeeId}/engagements.

curl -X POST https://api.wingspan.app/v3/payments/payees/Kzp8xceXy69Utto3wnouyQ/engagements \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "engagementId": "BmKH2r0PVv2JDxvmFA0fVn",
    "engagementType": "Employee",
    "externalId": "emp-1042-field-rep",
    "worksiteId": "RYQqqvt7IyyE1P5c3HDzmC",
    "startDate": "2026-11-09",
    "jobTitle": "Field representative"
  }'
// 201 Created (trimmed)
{
  "id": "j88P41LYjxNCLZ6CmGNHqH",
  "relationshipId": "Kzp8xceXy69Utto3wnouyQ",
  "engagementId": "BmKH2r0PVv2JDxvmFA0fVn",
  "engagementName": "Field representative, Los Angeles",
  "type": "Employee",
  "status": "Created",
  "startDate": "2026-11-09",
  "endDate": null,
  "externalId": "emp-1042-field-rep",
  "payerAccountId": "23Gvh0YtpH6bNj4iOxLqET",
  "payeeAccountId": "IJoFzShicUxKFl03UuRGSd",
  "worksiteId": "RYQqqvt7IyyE1P5c3HDzmC",
  "jobTitle": "Field representative",
  "areAllRequirementsComplete": false,
  "paymentsEligibility": "Eligible",
  "requirementIds": [
    "mZIjntKApi3UDgNnonMWgG",
    "biwRssDJkDNgFVdzjwA84X",
    "tmLZKi3qDcYK51FMMQnN7a",
    "tMaXu4Hv7Mi0VcXQrF6epz"
  ]
}
FieldRequiredWhat it does
engagementIdYesThe Employee engagement to assign them to.
engagementTypeYesEmployee. It has to match the engagement and the payee's context.
worksiteIdNoWhere they work. Its state decides which state taxes apply, and it has to be in a state registered on your employer payroll profile. Leave it out for a remote employee.
startDateNoTheir first day on this assignment, as YYYY-MM-DD.
endDateNoTheir last day, when you already know it.
jobTitleNoTheir job title.
externalIdNoYour own ID for the assignment.

When you create the assignment, Wingspan works out the employee's federal, state, and local taxes from their home address and worksite. An employee with no home address on file is taxed from your primary worksite.

Track what the employee still needs to complete

Read the assignment with ?expand=Requirements to see what's left. Every Employee assignment starts with these four requirements. At a California worksite, none of them hold the employee's pay: California requires wages on the regular payday, so the assignment is Eligible from the start, and Wingspan withholds at the default elections until the employee completes their tax forms. At other worksites, each one blocks payment until it's complete.

curl https://api.wingspan.app/v3/payments/payees/Kzp8xceXy69Utto3wnouyQ/engagements/j88P41LYjxNCLZ6CmGNHqH?expand=Requirements \
  -H "Authorization: Bearer $WINGSPAN_TOKEN"
Requirement typeNameWhat the employee does
RegistrationSign upCreates their Wingspan login, or has an account you linked.
TaxVerificationTax VerificationConfirms their legal name and Social Security number.
PayoutMethodPayout MethodAdds the bank account their pay is deposited to, or sets up a Wingspan Wallet.
EmployeeTaxFormsEmployee tax formsCompletes their Form W-4 and their state withholding certificate.

Employees complete these in Wingspan, or in your own app with embedded onboarding. Each requirement shows PendingCompletion until it's done. At other worksites, paymentsEligibility changes from NotEligible once every blocking requirement is complete. Any requirements you added to the engagement appear in the same list. See Requirements and eligibility.

Tax elections

An assignment's tax elections hold the employee's Form W-4 elections and their state withholding certificate. Read them with GET /v3/payments/payees/{payeeId}/engagements/{payeeEngagementId}/tax-elections. Until the employee submits a W-4, withholding uses the defaults: single, with no adjustments, and the state's default rate.

// 200 OK (trimmed)
{
  "engagementId": "j88P41LYjxNCLZ6CmGNHqH",
  "federal": {
    "filingStatus": "Single",
    "isMultipleJobs": false,
    "dependentsAmount": 0,
    "otherIncome": 0,
    "deductions": 0,
    "isExempt": false
  },
  "state": [
    { "state": "CA", "filingStatus": "Single", "totalAllowances": 0, "additionalAmount": 0, "isExempt": false }
  ]
}

If you collect W-4 elections in your own system, send them with PATCH .../tax-elections. Fields you leave out stay as they are. Sending elections doesn't complete the Employee tax forms requirement: the employee still completes and signs their forms.

curl -X PATCH https://api.wingspan.app/v3/payments/payees/Kzp8xceXy69Utto3wnouyQ/engagements/j88P41LYjxNCLZ6CmGNHqH/tax-elections \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "federal": {
      "filingStatus": "MarriedFilingJointly",
      "isMultipleJobs": false,
      "dependentsAmount": 2000,
      "otherIncome": 0,
      "deductions": 0,
      "isExempt": false
    },
    "state": [
      { "state": "CA", "filingStatus": "MarriedOneIncome", "totalAllowances": 2 }
    ]
  }'
FieldWhat it is
federal.filingStatusSingle, MarriedFilingJointly, HeadOfHousehold, or NonResidentAlien (Form W-4 Step 1c).
federal.isMultipleJobsThe Step 2 checkbox for multiple jobs or a working spouse.
federal.dependentsAmountThe Step 3 total for dependents and other credits, in dollars.
federal.otherIncomeStep 4(a), other income, in dollars per year.
federal.deductionsStep 4(b), deductions, in dollars per year.
federal.isExemptThe employee claims exemption from federal income tax withholding.
stateOne entry per state withholding certificate. California (CA) takes the employee's DE 4: filingStatus (Single, MarriedOneIncome, or HeadOfHousehold), totalAllowances, additionalAmount per pay period, and isExempt.

What can go wrong

Status and codeCauseFix
422 ValidationError with profile RequiredYou sent the email at the top level of the create request.Send it as profile.email.
403 AccountMismatch on POST /v3/platform/accountsThe session is bound to an account. Account creation doesn't accept account-bound sessions.Unbind the session with PATCH /v3/platform/sessions/{sessionId} and { "accountScope": null }, use the token it returns, and don't send X-Wingspan-Account.
409 ResourceConflict on the assignmentThe payee's context doesn't match engagementType.Assign Employee payees to Employee engagements only.
422 on the assignmentengagementType doesn't match the engagement.Send the engagement's own type.
422 payments.WorksiteJurisdictionNotEnabledThe worksite's state isn't registered on your employer payroll profile.Use a worksite in a registered state, or ask your Wingspan team to add the state.
404 on tax electionsThe payee isn't linked to an account yet, or the assignment isn't an Employee engagement.Link the employee's account first.

Related pages


Did this page help you?