Embed Wingspan in your app
Run Wingspan contractor onboarding and payments inside your own product with one credential, child Accounts per customer, and Account-scoped sessions.
This page shows how a platform runs Wingspan inside its own product: how to model your customers as Accounts, which credential your backend uses, how to act on each customer, and which onboarding steps Wingspan hosts for you. For a complete end-to-end walkthrough, see the embedded multi-tenant partner recipe.
Pick an integration style
| Style | What you build | What Wingspan hosts |
|---|---|---|
| Invite and claim | Create payees and payables through the API. | The invite email, contractor sign-up, and the contractor's own onboarding. |
| API-driven | Your own screens for everything you can collect through the API, plus your backend calls. | Individual steps that need Wingspan's UI, such as identity verification in an iframe. |
| Pre-built onboarding and payout-settings embeds | An iframe in your app. | The full contractor onboarding and payout-settings screens. |
The pre-built onboarding and payout-settings embeds currently run on the V1 API, and we're migrating them to V3. The components will look much the same, although the API data model underneath is different. If you want to use them, your Wingspan account team can share the current SDK docs and set you up with a V3 sandbox to build the rest of your integration while the V3 version is completed. The invite-and-claim and API-driven styles work with V3 today.
Model your customers as Accounts
flowchart TD R[Your platform's root Account] --> C1[Customer Account: Northwind Staffing] R --> C2[Customer Account: Harbor Clinics] C1 --> P1[Payee: Priya Shah] C2 --> P2[Payee: Priya Shah] P1 -.resolves to.-> K[Priya's own Account] P2 -.resolves to.-> K
- Your platform has a root Account.
- Each customer that pays contractors gets a child Account under your root. The customer, not your platform, is the payer, so payables, bank accounts, and 1099s belong to the customer's Account. See Parent and child Accounts.
- Each contractor is a Payee (called a collaborator in V1) owned by the customer's Account. A Payee can be paid before the contractor ever signs in. When the contractor claims the invite, the Payee links to the contractor's own Account. A contractor who works for two of your customers has two Payees and one Account. See Payees.
Set up your backend credential
Your backend authenticates as a ServiceAccount, a machine identity with no login. A ServiceAccount owned by your root Account covers that Account and all its descendants, so one API key reaches every customer. You authenticate once and pick the customer on each call with X-Wingspan-Account.
Creating a ServiceAccount and its API key requires a person signed in directly (not an API key and not an impersonation session) with a recent step-up MFA check. Without it you get 403 with code: StepUpMfaRequired.
1. Create the ServiceAccount
Create a ServiceAccount with the capabilities your backend needs. Direct scopes are written domain.resource:read or domain.resource:write.
curl -X POST https://api.wingspan.app/v3/platform/service-accounts \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Platform backend",
"ownerType": "Account",
"ownerId": "Nw7kQ2pLx9RtVb3mHc5dZa",
"scopes": ["payments.payee:write", "payments.payable:write"]
}'// 201 Created (trimmed)
{
"id": "Sv8cAe1mRk5tNq3pZx7bHd",
"status": "Created",
"ownerType": "Account",
"ownerId": "Nw7kQ2pLx9RtVb3mHc5dZa"
}The ServiceAccount starts in Created so you can review its scopes. Activate it with POST /v3/platform/service-accounts/{serviceAccountId}/activate.
2. Create an API key
Create an API key owned by the ServiceAccount:
curl -X POST https://api.wingspan.app/v3/platform/api-keys \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Platform backend (production)",
"ownerType": "ServiceAccount",
"ownerId": "Sv8cAe1mRk5tNq3pZx7bHd"
}'// 201 Created (trimmed)
{
"id": "Ak2yLp6wQs9dVm4xRt1cNb",
"ownerType": "ServiceAccount",
"ownerId": "Sv8cAe1mRk5tNq3pZx7bHd",
"secret": "<returned once>"
}Important: The
secretis returned once and never again. Store it in your secrets manager before doing anything else. If the response is lost, retrying with the sameIdempotency-Keyreturns409 ResourceConflictinstead of the secret; rotate the key withPOST /v3/platform/api-keys/{apiKeyId}/rotate.
Send the secret as Authorization: Bearer <secret>.
Act on a customer Account
Every call from your backend names the customer it's for:
curl -X POST https://api.wingspan.app/v3/payments/payees \
-H "Authorization: Bearer $WINGSPAN_API_KEY" \
-H "X-Wingspan-Account: Ap4tYs8KqW2nLm6xRb1cVe" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "email": "[email protected]", "context": "Contractor", "externalId": "northwind-c-1042" }'The payee is owned by the customer's Account. Rate limits are charged to the customer's Account and its Organization, not to your root, and webhooks for that payee are routed to the customer's Account. See Acting on behalf of Accounts.
Account sessions for a ServiceAccount
Some flows need a bearer token that is already bound to one Account, for example a short-lived token you hand to a browser component for a single customer. A ServiceAccount with the platform.account-session:write scope can mint one with Create an account-scoped session. X-Wingspan-Account is required and must equal the path Account.
curl -X POST https://api.wingspan.app/v3/platform/accounts/Ap4tYs8KqW2nLm6xRb1cVe/sessions \
-H "Authorization: Bearer $WINGSPAN_API_KEY" \
-H "X-Wingspan-Account: Ap4tYs8KqW2nLm6xRb1cVe" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "shouldIncludeDescendants": false }'// 201 Created (trimmed)
{
"id": "Ss1dKw6tQm3pXn8vLr2bHc",
"sessionType": "Account",
"boundAccountId": "Ap4tYs8KqW2nLm6xRb1cVe",
"isAccountScopeInclusiveOfDescendants": false,
"personId": null,
"expiresAt": "2026-09-24T18:00:00Z",
"token": "<opaque>"
}- The session lasts at most one hour. Set
expiresAtto shorten it; anything later than one hour returns422. - The token acts on the bound Account (and its descendants, if you opted in). A header pointing elsewhere returns
403 AccountMismatch. - The session represents an Account, not a person, so Person-only endpoints reject it with
400 AccountScopeNotApplicable. - Treat the token like a password. Never log it.
Onboard contractors
Invite and let the contractor claim
-
Create the Payee as the customer (shown above). This doesn't create an Account or Person for the contractor.
-
Send the invite with Invite a payee. Wingspan emails the contractor. You can add a plain-text
customMessage.curl -X POST https://api.wingspan.app/v3/payments/payees/Py3mQw7kLx2tRb9nVd4sHa/invite \ -H "Authorization: Bearer $WINGSPAN_API_KEY" \ -H "X-Wingspan-Account: Ap4tYs8KqW2nLm6xRb1cVe" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "customMessage": "Set up payments for your Northwind Staffing shifts." }' -
The contractor signs in, creates or picks their own Account, and accepts the payer. The Payee's
linkRequestStatusmoves fromPendingtoLinkedandpayeeAccountIdis filled in. Details are in Payee account linking.
The invite link goes to the contractor only. It is never returned in an API response, so you can't deliver it yourself.
Bind a contractor Account you already control
If your platform already controls the contractor's Account (for example, you migrated contractors with their consent), a person with access to both the customer Account and the contractor Account can link them without an invite using Associate a payee with an Account. The authority block records why the platform is allowed to do this (type: PlatformAsserted or MigrationImported, plus a basis). This action isn't available to ServiceAccounts yet.
Steps the contractor completes in their own session
Identity verification and bank linking run in the contractor's own signed-in session, because they act on the contractor's identity and money:
- Identity verification.
POST /v3/onboarding/identity-verificationscan return a short-livedsessionUrl(valid for 5 minutes) that you load in an iframe. Refresh an expired URL withPOST /v3/onboarding/identity-verifications/{identityVerificationId}/resume. See Identity verification. - Bank account linking.
POST /v3/finance/external-bank-accounts/linkreturns a token for the bank-linking UI. It requires a recent step-up MFA check. See Payout methods.
Know when a contractor can be paid
You can subscribe to Payee.Activated, which fires when a payee records an Activated transition (for example, after binding a contractor Account you already control). It isn't a signal that an invite was accepted, and webhooks for payee linking and requirement completion aren't available yet. Poll for those:
GET /v3/payments/payees?filter[linkRequestStatus][eq]=Linkedto find newly claimed payees.GET /v3/payments/payee-engagementsand readpaymentsEligibility(EligibleorNotEligible) on each engagement.
See Requirements and eligibility.
Receive events for every customer
Subscribe once, on your root Account, and include descendants:
curl -X POST https://api.wingspan.app/v3/platform/webhooks \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/wingspan",
"subscribedEvents": ["Payable.*", "Invoice.*", "FundsMovement.*"],
"scope": { "type": "Account", "id": "Nw7kQ2pLx9RtVb3mHc5dZa", "shouldIncludeDescendants": true }
}'Route each event by its routingSubject.id, which is the customer Account it's about. Push delivery is best effort: dedupe on the event id and recover missed events from GET /v3/platform/events. See Webhooks overview and Recover missed events.
Common mistakes
- Creating customer Accounts with the API key. Account creation needs a Person session. Provision customer Accounts from an admin's login, then operate them with the ServiceAccount key.
- Putting the payer on your platform's Account. If the customer is the one paying contractors, the payables must live on the customer's Account, or 1099s and funding end up on the wrong entity.
- Sending
X-Wingspan-Accountto Person-only endpoints. They return400 AccountScopeNotApplicable. - Waiting for a webhook that doesn't exist yet. Payee linking and engagement eligibility need polling today.
- Planning to re-parent customers. Parents are fixed at creation. Create each customer Account under the right parent the first time.
Related pages
Updated 10 days ago