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

ResourceIn the Wingspan appWhat it is
EngagementEngagementA 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.
PayeeEngagementAssignmentOne 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" }
}
FieldWhat it's for
nameRequired.
description, externalId, metadataYour labels and IDs. A duplicate externalId returns 409 ResourceConflict.
engagementTypeContractor, Employee, EmployeeOfRecord, or AgentOfRecord. See Engagement types and payee context.
requirementDefinitionIdsRequirement 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.
rateCardIdBase rate card for this engagement.
worksiteId, isRemoteLocation, 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

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:

blockingModeEffect of an incomplete requirement
BlocksPaymentThe Payee can't be paid through this engagement.
BlocksEligibilityButNotPaymentMarks the engagement not eligible, without blocking payment.
NotBlockingTracked, 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

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:

engagementTypeWhat it is
ContractorA 1099 contractor or vendor relationship. No withholding.
EmployeeA W-2 employee on payroll, with withholding.
EmployeeOfRecordAn employer-of-record engagement, where Wingspan or a partner is the legal employer.
AgentOfRecordAn 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

StatusMeaning
CreatedAssigned, not yet in service.
ActivatedIn service.
SuspendedPaused. Can be reactivated.
TerminatedEnded. 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
ActionCallNotes
ActivatePOST .../engagements/{payeeEngagementId}/activateFrom Created or Suspended. Returns 409 if eligibility is incomplete.
SuspendPOST .../suspendFrom Activated. Optional {"reason": "..."}.
TerminatePOST .../terminateFrom Activated or Suspended. Permanent.
DeleteDELETE .../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:

  1. Terminate (or, if never in service, delete) every Created or Activated engagement of the current context.
  2. Call POST /v3/payments/payees/{payeeId}/change-context with the new context. See Payees.
  3. 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. DELETE only works in Created. Use terminate.
  • Trying to change engagementType. Terminate and create a new PayeeEngagement. Change the Payee's context first 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. activate returns 409 with payeeEngagement.EligibilityIncomplete until blocking requirements are complete.

Related


Did this page help you?