Engagements
Define reusable Engagement templates, assign Payees to them with PayeeEngagements, and move an engagement through its lifecycle in the V3 API.
An engagement describes a piece of work and the terms a Payee works under. This page shows you how to create an Engagement template, assign a Payee to it, and activate, suspend, or end that assignment.
Three resources, three jobs
| Resource | In the Wingspan app | What it is |
|---|---|---|
Engagement | Engagement | A reusable template you define once, such as "Property Inspections" or "ICU Travel Nursing". It holds the requirements, work definitions, and rate card that apply to everyone on it. |
PayeeEngagement | Assignment | One Payee assigned to one Engagement. It carries the classification (engagementType), dates, overrides, the requirement instances, and whether the Payee can be paid through it. |
PayerEngagement | (client project) | The other direction: a commercial engagement with a client who pays you. See PayerEngagement below. |
Wingspan's February 2026 terminology update renamed "Engagement Type" to Engagement and "Contractor Engagement" to Assignment. In the API, the Engagement is Engagement and the Assignment is PayeeEngagement.
A Payee can hold several PayeeEngagements at once, including under different Engagements. Payables, requirements, and eligibility are tracked per PayeeEngagement, which is why "can I pay this person?" is answered on the engagement and not on the Payee.
Create an engagement template
POST /v3/payments/engagements:
curl -X POST https://api.wingspan.app/v3/payments/engagements \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "ICU Travel Nursing",
"description": "13-week ICU contracts, west region",
"externalId": "PRJ-ICU-WEST",
"engagementType": "Contractor",
"requirementDefinitionIds": ["oP8lYOzdKQy7H_mNrx9Uf9"]
}'// 201 Created (trimmed)
{
"id": "Fj9ZSOWd3LdiamTunQ5x98",
"name": "ICU Travel Nursing",
"externalId": "PRJ-ICU-WEST",
"status": "Active",
"isDefault": false,
"engagementType": "Contractor",
"requirementIds": ["oP8lYOzdKQy7H_mNrx9Uf9"],
"requirementDefinitions": [
{ "requirementDefinitionId": "oP8lYOzdKQy7H_mNrx9Uf9", "blockingMode": "BlocksPayment" }
],
"workDefinitionIds": [],
"events": { "createdAt": "2026-09-24T15:20:00Z" }
}| Field | What it's for |
|---|---|
name | Required. |
description, externalId, metadata | Your labels and IDs. A duplicate externalId returns 409 ResourceConflict. |
engagementType | Contractor, Employee, EmployeeOfRecord, or AgentOfRecord. See Engagement types and payee context. |
requirementDefinitionIds | Requirement definitions every Payee on this engagement must complete. See Requirements and eligibility. |
workDefinitions | [{ "workDefinitionId": "...", "rateCalculationId": "..." }] to turn on work logs for this engagement. See Work logs and rate cards. |
rateCardId | Base rate card for this engagement. |
worksiteId, isRemote | Location, for Employee work: a worksite for onsite work, or isRemote: true for remote work. |
Your Account also has a default Engagement that Wingspan provisions for you (isDefault: true). You can't create one with isDefault: true; that returns 422.
Attach or remove requirements later
POST /v3/payments/engagements/{engagementId}/requirementswith{"requirementDefinitionId": "...", "blockingMode": "BlocksPayment"}. Returns204.DELETE /v3/payments/engagements/{engagementId}/requirements/{requirementId}. Returns204.
These sub-resources are write-only. Read the result back with GET /v3/payments/engagements/{engagementId} and look at requirementDefinitions. blockingMode controls what an unfinished requirement blocks:
blockingMode | Effect of an incomplete requirement |
|---|---|
BlocksPayment | The Payee can't be paid through this engagement. |
BlocksEligibilityButNotPayment | Marks the engagement not eligible, without blocking payment. |
NotBlocking | Tracked, but blocks nothing. |
The same pattern works for work definitions: POST /v3/payments/engagements/{engagementId}/work-definitions with {"workDefinitionId": "..."} to attach, and DELETE /v3/payments/engagements/{engagementId}/work-definitions/{workDefinitionId} to detach.
Update, archive, and delete
PATCH /v3/payments/engagements/{engagementId}updatesname,description,externalId,rateCardId,worksiteId,isRemote, andmetadata. Omitted fields stay the same.POST .../archivesetsstatustoArchived. Active PayeeEngagements block this with409.POST .../restoresets it back toActive.DELETE /v3/payments/engagements/{engagementId}removes the template. It returns409if any active PayeeEngagement uses it.
List templates with GET /v3/payments/engagements, filtering by status (anyOf), externalId, isDefault, or requirementDefinitionIds (anyOf), and sorting by one of sort[name] or sort[updatedAt]. The requirementDefinitionIds filter finds every template that uses a requirement definition, which is useful before you retire that definition.
Assign a payee to an engagement
POST /v3/payments/payees/{payeeId}/engagements creates the PayeeEngagement. The Payee doesn't need to be linked first.
curl -X POST https://api.wingspan.app/v3/payments/payees/M9ISYbzJElXs4zIHv76rjT/engagements \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"engagementId": "Fj9ZSOWd3LdiamTunQ5x98",
"engagementType": "Contractor",
"startDate": "2026-10-05",
"endDate": "2027-01-02",
"externalId": "ASSIGN-77120",
"jobTitle": "ICU RN"
}'// 201 Created (trimmed)
{
"id": "ru9zEafz6i4WRlIOA10wAk",
"engagementId": "Fj9ZSOWd3LdiamTunQ5x98",
"engagementName": "ICU Travel Nursing",
"payeeId": "M9ISYbzJElXs4zIHv76rjT",
"engagementType": "Contractor",
"status": "Created",
"startDate": "2026-10-05",
"endDate": "2027-01-02",
"paymentsEligibility": "NotEligible",
"areAllRequirementsComplete": false,
"requirementIds": ["CkAmZjWbwGy8H6bX6a4lA1"]
}Creating the PayeeEngagement provisions one requirement instance per requirement definition on the template. requirementIds lists them. engagementId and engagementType are required. engagementName lets you override the displayed name, for example with a job title. For Employee work, set worksiteId to a worksite for onsite work, or leave it out for remote work.
engagementType must match the template's type, or you get 422. It must also fit the Payee's context, or you get 409 ResourceConflict (see the next section).
Engagement types and payee context
engagementType is one of the EngagementType values:
engagementType | What it is |
|---|---|
Contractor | A 1099 contractor or vendor relationship. No withholding. |
Employee | A W-2 employee on payroll, with withholding. |
EmployeeOfRecord | An employer-of-record engagement, where Wingspan or a partner is the legal employer. |
AgentOfRecord | An agent-of-record engagement, where an agent administers the engagement for the payer. |
Every Payee has a context, Contractor or Employee, that you set when you create it. Only an Employee engagement fits the Employee context, and the other types fit Contractor. Creating a PayeeEngagement whose type doesn't fit the Payee's context returns 409 ResourceConflict. A Payee created before context existed takes its context from its first engagement.
Lifecycle
| Status | Meaning |
|---|---|
Created | Assigned, not yet in service. |
Activated | In service. |
Suspended | Paused. Can be reactivated. |
Terminated | Ended. Terminal. |
stateDiagram-v2
[*] --> Created: POST /payees/{payeeId}/engagements
Created --> Activated: POST /activate
Activated --> Suspended: POST /suspend
Suspended --> Activated: POST /activate
Activated --> Terminated: POST /terminate
Suspended --> Terminated: POST /terminate
Created --> [*]: DELETE
| Action | Call | Notes |
|---|---|---|
| Activate | POST .../engagements/{payeeEngagementId}/activate | From Created or Suspended. Returns 409 if eligibility is incomplete. |
| Suspend | POST .../suspend | From Activated. Optional {"reason": "..."}. |
| Terminate | POST .../terminate | From Activated or Suspended. Permanent. |
| Delete | DELETE .../engagements/{payeeEngagementId} | Only while Created. Use terminate once an engagement has been in service. |
All four paths start with /v3/payments/payees/{payeeId}/engagements/{payeeEngagementId}.
When a lifecycle action returns 409, detailCode tells you why, for example payeeEngagement.EligibilityIncomplete (requirements still open) or payeeEngagement.InvalidLifecycleTransition (wrong current status). Use it for logs and support; branch your code on code.
Changing a Payee's classification
engagementType can't be changed on an existing PayeeEngagement. To move someone from one classification to another, terminate the current PayeeEngagement and create a new one with the new type. The Payee, its payment history, and your externalId mapping stay the same.
Moving between contractor and employee work also changes the Payee's context:
- Terminate (or, if never in service, delete) every
CreatedorActivatedengagement of the current context. - Call
POST /v3/payments/payees/{payeeId}/change-contextwith the newcontext. See Payees. - Create the new PayeeEngagement.
A Suspended engagement doesn't block the change, but it can't be resumed afterward, so terminate it if you're done with it.
Employment types (Employee, EmployeeOfRecord) have extra prerequisites, including a linked Account for the Payee. See Payroll runs.
Read engagements
One Payee's engagements: GET /v3/payments/payees/{payeeId}/engagements, filterable by externalId.
All engagements across your Payees: GET /v3/payments/payee-engagements, filterable by status (eq, anyOf), engagementType, engagementId (eq, anyOf), and externalId.
curl -G https://api.wingspan.app/v3/payments/payee-engagements \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
--data-urlencode "filter[engagementId][eq]=Fj9ZSOWd3LdiamTunQ5x98" \
--data-urlencode "filter[status][anyOf][]=Created" \
--data-urlencode "filter[status][anyOf][]=Activated"One engagement with its requirements inline: GET /v3/payments/payee-engagements/{payeeEngagementId}?expand=Requirements. Each inlined requirement carries the blockingMode this engagement applies to it. expand=WorkDefinitions inlines the template's work definitions. Expansions work on single reads only, not on lists.
Update an engagement
PATCH /v3/payments/payees/{payeeId}/engagements/{payeeEngagementId} or the flat PATCH /v3/payments/payee-engagements/{payeeEngagementId} updates startDate, endDate (send null to clear), jobTitle, engagementName, worksiteId, overrides, and metadata.
overrides.isAutoPayEnabled overrides the Payee's defaults.isAutoPayEnabled for this engagement only.
To bind a rate card that overrides the template's for one Payee, use PATCH .../engagements/{payeeEngagementId}/rate-card with {"rateCardId": "..."}.
PayerEngagement for client relationships
If you also bill clients, a PayerEngagement records a commercial engagement with a client who pays you. Create it against your Payer record with POST /v3/payments/payers/{payerId}/engagements, using type: Commercial, an engagementId from your own Account, and a startDate.
A PayerEngagement is its own resource with its own ID and fields. It isn't a mirror of a PayeeEngagement, and it doesn't carry worker requirements or eligibility. See Invoices overview.
automaticCollectionMode (Inherit, Enabled, or Disabled) on a PayerEngagement overrides the Payer's isAutomaticInvoiceCollectionEnabled for that engagement, so Wingspan collects (or doesn't) on your opened invoices without a pay call for each. See Collect payment. Auto-pay on the paying side is different: it still comes from Payee.defaults.isAutoPayEnabled and the PayeeEngagement's overrides.isAutoPayEnabled.
Webhooks
Engagement.*, PayeeEngagement.*, and PayerEngagement.* events aren't available for subscription yet, including PayeeEngagement.EligibilityChanged. Poll GET /v3/payments/payee-engagements with filters until they are.
Common mistakes
- Deleting an engagement that's been in service.
DELETEonly works inCreated. Useterminate. - Trying to change
engagementType. Terminate and create a new PayeeEngagement. Change the Payee'scontextfirst if you're moving between contractor and employee work. - Reading requirements from the template. The template lists requirement definitions. The Payee's actual progress is on the PayeeEngagement's requirement instances.
- Activating before requirements are done.
activatereturns409withpayeeEngagement.EligibilityIncompleteuntil blocking requirements are complete.
Related
Updated 10 days ago