Gig marketplaces
How delivery, mobility, and on-demand marketplaces onboard and pay large contractor pools with the Wingspan V3 API. Account model, daily pay cycle, and pitfalls.
This guide is for marketplaces that pay many contractors often: delivery, ride-hailing and mobility, home services, and freelance platforms. It covers how to structure markets as Accounts, how to onboard a large pool (including migrating an existing one), how to run a daily pay cycle, and what to listen for.
Typical requirements
- Thousands of contractors, high turnover, and seasonal surges in onboarding.
- Pay that's calculated daily or per job, often from a dispatch or trip system.
- Several operating companies (one per city or region), each with its own tax ID, so payments and 1099s must be attributed to the right entity.
- Migrating an existing contractor base without asking everyone to start over.
- Handling returned payments without manual spreadsheets.
Structure markets as Accounts
If each market pays contractors under its own tax ID, give each one a child Account under a parent:
flowchart TD P[Parent Account: Northwind Mobility] --> M1[Market Account: Indianapolis] P --> M2[Market Account: Albuquerque] M1 --> D1[Payee: driver] M2 --> D2[Payee: driver]
Your backend uses one ServiceAccount key on the parent and names the market on each call with X-Wingspan-Account. Payables, funding, and year-end forms then belong to that market. A driver who works in two markets has a Payee in each and one Account of their own. See Parent and child Accounts and Embed Wingspan in your app.
A single Account is fine if all contractors are paid by one legal entity.
Map your objects
| Marketplace object | Wingspan object | Notes |
|---|---|---|
| Market or operating company | Child Account | externalId = your market code |
| Driver, courier, or provider | Payee on a Contractor engagement | externalId = your worker ID |
| Trip, delivery, or job | Line item on a payable | Use line-item custom fields for booking IDs |
| Daily or weekly earnings | Payable | externalId = your earnings record ID, so a retry can't pay twice |
| Amount the contractor owes you | Deduction | Taken from their pay |
| Payout sent | Payout, listed under the payable | Read with GET /v3/payments/payouts |
Onboard the contractor pool
New contractors. Create each Payee, place it on an engagement, and invite it. The contractor completes onboarding in Wingspan's hosted flow. See Invite a payee.
A surge or a migration. Use a PayeeImport batch. Each item matches an existing payee by payeeId, then externalId, then email, and updates it; otherwise it creates the payee and sends the invite. Set configuration.context to Contractor (it's required), and optionally configuration.engagementId to place every payee in the batch on one engagement. See Batches and bulk operations.
Contractors whose Account you already manage. If your platform controls the contractor's Wingspan Account (for example, from an earlier integration, with the contractor's agreement), a person with access to both Accounts can bind the Payee to it with POST /v3/payments/payees/{payeeId}/associate-account, recording the authority as MigrationImported or PlatformAsserted. The contractor doesn't have to accept a new invite. Talk to us before a large migration so we can check which existing verifications carry over.
Run a daily pay cycle
sequenceDiagram
participant D as Dispatch system
participant WS as Wingspan API
D->>WS: POST /v3/platform/batches (type PayableImport)
D->>WS: POST /batches/{id}/items, one per driver-day
D->>WS: POST /batches/{id}/process
D->>WS: GET /batches/{id}/summary until Completed
WS-->>D: Payable.Paid, Payable.Returned webhooks
- At the end of the day, create a
PayableImportbatch on the market's Account with aprocessingStrategy. - Add one item per payable. Give each item a
uniqueReferenceKey(for example, the driver ID plus the date), thepayeeEngagementId, adescription, and atotalCost. A negative amount creates a deduction against the payee instead of a payable. - Process the batch with
POST /v3/platform/batches/{batchId}/processand follow it withGET /v3/platform/batches/{batchId}/summary. Items that fail carry field-level errors; fix and resubmit them in a new batch. - Pay the resulting payables, or set
payableStatus: OpenedandpayerApprovalStatus: Approvedon the batch so they're ready for a contractor payroll run. See Bulk payables and Payroll runs.
For one-off payments (a bonus or an adjustment), create a single payable instead. See Create a payable.
Listen for results
| Event | What to do |
|---|---|
Payable.Paid | Mark the earnings record paid in your system. This is Wingspan's internal state, not confirmation that the driver's bank received the money. |
Payable.Returned | The payment came back. Returned is terminal: create a new payable after the driver fixes their payout method. |
FundsMovement.Failed, FundsMovement.Returned | Money-movement detail for reconciliation. |
Dedupe on the event id, and recover anything you missed from GET /v3/platform/events. Whether a driver can be paid (paymentsEligibility on the payee engagement) has no webhook yet; poll it before you create earnings for a new driver. See Paid vs DepositConfirmed and Recover missed events.
Contractor financial features
Contractors choose how they get paid, including faster payout methods where offered, in their own onboarding and settings. Debit cards and a tax withholding account are available on the contractor's own Account through /v3/finance/cards and /v3/finance/tax-withholdings. See Payout methods. Insurance and benefits products aren't available in the V3 API yet.
Pitfalls
- Paying the same day twice. Use a stable
externalIdoruniqueReferenceKeyper earnings record, and anIdempotency-Keyper request. - One market's payments on another's Account. Always send the market's
X-Wingspan-Account. Year-end forms follow the Account that paid. - Creating earnings for drivers who can't be paid yet. Check
paymentsEligibilityfirst, or expect those payables to wait. - Hitting rate limits during the daily run. Adding batch items is still one write call per item. Watch the
RateLimitheader, pace your calls, and back off on429usingRetry-After. See Rate limiting.
Related pages
Updated 10 days ago