Integrate an ATS or CRM

Connect an applicant tracking system or CRM to Wingspan V3. Object mapping, the calls to make on hire, how to sync status back, and common pitfalls.

This guide shows how to connect an applicant tracking system (ATS) or customer relationship management (CRM) system to Wingspan: which Wingspan objects correspond to candidates, placements, and clients; which calls to make when someone is hired; and how to send onboarding and payment status back. It's a pattern guide. You write the middleware (or use an integration tool) that calls the Wingspan API.

Where Wingspan fits

Staffing and contingent-work businesses usually run several systems:

  • Recruiting (ATS): finds candidates and tracks hires.
  • Client relationships (CRM): tracks the companies you bill.
  • Assignment management: tracks who works where.
  • Wingspan: onboards contractors, collects tax and payout information, pays them, and files 1099s.

The integration has two directions. Inbound, a hire or placement in the ATS creates or updates records in Wingspan. Outbound, Wingspan status (claimed, ready to pay, paid) flows back so recruiters and account managers know when a contractor can start.

Map your objects

ATS or CRM objectWingspan objectFields that matter
Hired candidatePayee (called a collaborator in V1)externalId = candidate ID, email, context (Contractor or Employee), profile.displayName
Custom candidate fields (license state, recruiter, cost center)Custom field with resourceType: PayeeValues set with PATCH /v3/payments/payees/{payeeId}/custom-fields
Job or placement typeEngagement (a reusable template)name, externalId, requirements
A specific placementPayeeEngagementengagementId, engagementType, externalId = placement ID, startDate, endDate, jobTitle
Client company you billPayerexternalId = CRM account ID, email
Client company that runs its own payroll through youA child AccountexternalId = CRM account ID
Timesheet or completed shiftPayable or work logexternalId = timesheet ID, line items
Client billInvoiceexternalId = your invoice ID

Put your system's ID in externalId, never in metadata. externalId is a first-class field: it's echoed on every read, filterable with filter[externalId][eq], and unique per Account and resource type, so it's how your middleware finds the Wingspan record again. Use metadata for extra tags you don't look records up by.

Inbound: a candidate is hired

sequenceDiagram
  participant ATS
  participant MW as Your middleware
  participant WS as Wingspan API
  ATS->>MW: Candidate status = Hired
  MW->>WS: POST /v3/payments/payees (externalId = candidate ID)
  MW->>WS: PATCH /payees/{id}/custom-fields
  MW->>WS: POST /payees/{id}/engagements (externalId = placement ID)
  MW->>WS: POST /payees/{id}/invite
  WS-->>ATS: (via middleware) payeeId stored on candidate

1. Create the payee

Create a payee. Derive the Idempotency-Key from the candidate ID so a retried webhook from your ATS can't create a second payee.

curl -X POST https://api.wingspan.app/v3/payments/payees \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: ats-hire-cand-88213" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "externalId": "cand-88213",
    "context": "Contractor",
    "profile": { "displayName": "Priya Shah" }
  }'
// 201 Created (trimmed)
{
  "id": "Py3mQw7kLx2tRb9nVd4sHa",
  "externalId": "cand-88213",
  "email": "[email protected]",
  "context": "Contractor",
  "payeeAccountId": null
}

context is required. Use Contractor for 1099 placements and Employee for W-2 hires. It has to fit the engagement type you place the payee on in step 3.

If the candidate was already sent (for example, rehired), you get 409 ResourceConflict. Create is never an upsert. Look up the existing payee and continue:

curl -G https://api.wingspan.app/v3/payments/payees \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  --data-urlencode "filter[externalId][eq]=cand-88213"

2. Copy custom fields

Define each field once with Create a custom field (resourceType: Payee, a key, and a dataType such as String). Then set values per payee with Update payee custom fields:

curl -X PATCH https://api.wingspan.app/v3/payments/payees/Py3mQw7kLx2tRb9nVd4sHa/custom-fields \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "licenseState": "NY", "recruiter": "Jordan Lee" }'

Send null for a key to clear it.

3. Place the payee on an engagement

Create an Engagement template once per job type with POST /v3/payments/engagements (it holds the onboarding requirements for that kind of work). Then attach the payee with Create a payee engagement:

curl -X POST https://api.wingspan.app/v3/payments/payees/Py3mQw7kLx2tRb9nVd4sHa/engagements \
  -H "Authorization: Bearer $WINGSPAN_TOKEN" \
  -H "Idempotency-Key: ats-place-pl-40177" \
  -H "Content-Type: application/json" \
  -d '{
    "engagementId": "En5tRk2wQp8xLm3nVb6dHs",
    "engagementType": "Contractor",
    "externalId": "pl-40177",
    "jobTitle": "Travel RN",
    "startDate": "2026-10-05"
  }'

The payee engagement carries the requirements the contractor must complete before you can pay them for this work.

4. Invite the contractor

Invite the payee. Wingspan emails the contractor, who signs up, completes onboarding, and accepts you as a payer. See Invite a payee.

Hiring many at once

For a migration or a large hiring wave, use a PayeeImport batch instead of one call per candidate. Each item matches an existing payee by payeeId, then externalId, then email, and updates it; otherwise it creates the payee and invites it. Set configuration.context (required) to the context new payees are created with, and configuration.engagementId to place every payee on one engagement. See Batches and bulk operations.

Outbound: send status back to the ATS or CRM

Recruiters want to know when a contractor has claimed the invite and is ready to be paid. Webhooks for payee linking, engagement eligibility, and requirement completion aren't available yet, so poll on a schedule:

You want to knowPoll
The contractor claimed the inviteGET /v3/payments/payees?filter[linkRequestStatus][eq]=Linked
The contractor can be paid for a placementGET /v3/payments/payee-engagements?filter[externalId][eq]=pl-40177, then read paymentsEligibility (Eligible or NotEligible) and areAllRequirementsComplete
What changed on a payeeGET /v3/payments/payees/{payeeId}/events, newest first

Payment status is available as webhooks. Subscribe to Payable.Paid, Payable.Returned, and, if you bill clients through Wingspan, Invoice.Paid and Invoice.PaymentFailed. Map each event back with the resource's externalId: fetch the payable or invoice by data.id and read externalId. See Webhooks overview.

Example: write verification status to a CRM vendor record

A company that uses Wingspan to verify vendors at registration keeps its CRM as the system of record. Its middleware:

  1. Creates the payee with externalId set to the CRM vendor record ID.
  2. Polls the payee's engagement until paymentsEligibility changes, or until a requirement needs attention.
  3. Writes the result and the timestamp back to the CRM record, looking it up by externalId.

Because externalId comes back on every read, the middleware needs no lookup table of its own.

Pitfalls

  • Treating create as upsert. A duplicate externalId returns 409. Handle it by looking the record up, not by retrying with a new ID.
  • Reusing an Idempotency-Key with a different body. That returns 409 IdempotencyKeyConflict. Tie the key to the source event, not to the candidate alone, when the same candidate can generate different requests. The response cache lasts 24 hours and is best effort. See Idempotency.
  • Polling too hard. Filter on linkRequestStatus, status, or externalId instead of listing everything, and page with page[token]. Reads cost from the account-read bucket. See Rate limiting.
  • Assuming Payable.Paid means money arrived. Paid is Wingspan's internal state. It doesn't mean the contractor's bank has received the funds. See Paid vs DepositConfirmed.
  • Ignoring errors[]. Validation failures return 422 with per-field errors[]. Log them against the source record so someone can fix the data in the ATS. See Errors.

Related pages


Did this page help you?