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 components (Embedded SDK) | A few lines of frontend code, plus a backend endpoint that creates sessions. | Screens for payees (onboarding, payout method, tax verification fixes, Wallet cards) and payers (onboarding, payment method and autopay, payroll funding). See Embedded SDK. |
The pre-built components have screens for payees and payers, for both accounts payable and accounts receivable. They run on V3 with Embedded SDK 2.x. Your backend creates a short-lived Account session with its ServiceAccount key, and the SDK renews it as needed. See Embedded SDK for setup and the full component reference.
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.
If you call with a person's login session, creating a ServiceAccount and its API key needs a recent MFA step-up (ApiKeyChange). Without it you get 403 with code: StepUpMfaRequired. Impersonation sessions can't do this. A backend calling with an API key has no session to step up.
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 '{ "profile": { "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 created it under the customer's Account, or you migrated contractors with their consent), link them without an invite using Associate a payee with an Account. Your ServiceAccount key can make this call when both Accounts are inside its tree, with the payments.payee-identity:write scope. A person with access to both Accounts can make it too. The authority block records why the platform is allowed to do this (type: PlatformAsserted or MigrationImported, plus a basis).
Steps the contractor completes in their own session
Identity verification and bank linking run in the contractor's own session, because they act on the contractor's identity and money. With the Embedded SDK, the onboarding and payout method components do this inside your app on an Account session your backend creates, so the contractor never signs in to Wingspan:
- Identity verification.
POST /v3/onboarding/identity-verificationscreates the verification.POST /v3/onboarding/identity-verifications/{identityVerificationId}/resumereturns a short-livedsessionTokenthat your frontend passes to the identity provider's SDK. It isn't a URL. Callresumeagain for a fresh token. See Identity verification. - Bank account linking.
POST /v3/finance/external-bank-accounts/linkreturns a token for the bank-linking UI. If you call with a person's login session, it needs a recent MFA step-up (BankAccountChange). A backend calling with an API key has no session to step up. See Payout methods.
Know when a contractor can be paid
You can subscribe to Payee.Activated, which fires when POST /v3/payments/payees/{payeeId}/activate reactivates a Deactivated Payee. Creating a Payee, an accepted invite, and binding a contractor Account you already control don't fire it. You can also subscribe to PayeeEngagement.EligibilityChanged, which fires when an engagement's paymentsEligibility flips. To track invites and payment eligibility, read these lists:
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 under the wrong parent. A ServiceAccount key can create child Accounts, with
parentAccountIdset andX-Wingspan-Accountnaming that parent. It can't create a root Account. The key needsusers.account:writeand Write authority that reaches new children. - 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. - Treating
Payee.Activatedas invite acceptance. ChecklinkRequestStatuson the Payee to see whether the contractor accepted, andpaymentsEligibilityon each engagement to see whether you can pay them. - Planning to re-parent customers. Parents are fixed at creation. Create each customer Account under the right parent the first time.
Related pages
Updated about 1 hour ago