Connect QuickBooks Online
Connect a QuickBooks Online company to a Wingspan Account, map vendors, customers, and accounts, and audit what syncs, using the V3 API.
This page walks through connecting a QuickBooks Online (QBO) company to a Wingspan Account through the V3 API, setting the default mappings, linking payees and payers to QBO vendors and customers, and checking what synced. Once connected, Wingspan writes your payables to QBO as Bills and your invoices as QBO Invoices, and records their payments.
You can also connect QuickBooks from Settings > Integrations in the Wingspan app. The steps below are the API equivalent.
How it works
An AccountingConnection links one Wingspan Account to one QBO company. Each Account can have one active QuickBooks connection. The connection has two separate status fields:
| Field | Values | Meaning |
|---|---|---|
status | Created, Connecting, Connected, RetryingConnection, Disconnected | The OAuth link to QuickBooks. |
syncState | NeverSynced, Syncing, Synced, OutOfSync | How fresh the books are. Derived by Wingspan; you never set it. |
Wingspan keeps a normalized, read-only copy of your QBO reference data (vendors, customers, items, classes, and chart of accounts). You map Wingspan records to those copies by their Wingspan IDs, not by QBO's native IDs.
sequenceDiagram
participant App as Your app
participant WS as Wingspan API
participant QBO as QuickBooks
App->>WS: POST /accounting-connections
App->>WS: POST /{id}/connect
WS-->>App: authorizeUrl, state
App->>QBO: Send the user to authorizeUrl
QBO-->>App: Redirect with code, state, realmId
App->>WS: POST /{id}/complete-connection
App->>WS: POST /{id}/refresh-reference-data
App->>WS: GET vendors, items, classes, ledger-accounts
App->>WS: PATCH /{id} mappingDefaults
Note over WS,QBO: Payables and invoices sync to QBO
Prerequisites
- An Account you can act on. Accounting connections are Account-scoped: send
X-Wingspan-Accountto connect a child Account. - A QuickBooks Online user who can authorize the company.
1. Create the connection
Create an accounting connection:
curl -X POST https://api.wingspan.app/v3/platform/accounting-connections \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "partner": "Quickbooks", "externalId": "qbo-main" }'// 201 Created (trimmed)
{
"id": "Qc4mTn7wKp2xRb8dLs5vHa",
"partner": "Quickbooks",
"accountId": "Nw7kQ2pLx9RtVb3mHc5dZa",
"status": "Created",
"syncState": "NeverSynced"
}A second active QuickBooks connection on the same Account returns 409 with detailCode: integrations.PartnerAlreadyConnected.
2. Authorize with QuickBooks
Call Connect to get the QuickBooks authorization URL:
curl -X POST https://api.wingspan.app/v3/platform/accounting-connections/Qc4mTn7wKp2xRb8dLs5vHa/connect \
-H "Authorization: Bearer $WINGSPAN_TOKEN"{
"authorizeUrl": "<QuickBooks authorization URL>",
"state": "<opaque>",
"expiresAt": "2026-09-24T15:20:00Z"
}Send the user to authorizeUrl. They sign in to Intuit and pick the QBO company. When QuickBooks redirects back, pass the code, state, and realmId it returned to Complete the connection:
curl -X POST https://api.wingspan.app/v3/platform/accounting-connections/Qc4mTn7wKp2xRb8dLs5vHa/complete-connection \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "code": "<from redirect>", "state": "<from redirect>", "realmId": "<from redirect>" }'The response shows status: Connected and partnerCompanyName, and Wingspan fires AccountingConnection.Connected. The state is single-use and expires: an invalid or expired one returns 422 with detailCode: integrations.OAuthStateInvalid. A realmId that doesn't match the company that started the flow returns 422 with integrations.RealmMismatch. In either case, call connect again.
3. Pull QuickBooks reference data
Refresh reference data copies the collections you name into Wingspan:
curl -X POST https://api.wingspan.app/v3/platform/accounting-connections/Qc4mTn7wKp2xRb8dLs5vHa/refresh-reference-data \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "collections": ["LedgerAccounts", "Customers", "Vendors", "Items", "Classes"] }'The response is 202 with an async operation (operationType: ReferenceDataRefresh). Today the work finishes before the response returns, so the operation is usually already Completed, PartiallyCompleted, or Failed. One collection failing doesn't block the others; check progress and error, and refresh the failed collection again.
Then read the copies:
| Collection | Call |
|---|---|
| Chart of accounts | GET .../ledger-accounts |
| Items (products and services) | GET .../items |
| Classes | GET .../classes |
| Vendors | GET .../vendors |
| Customers | GET .../customers |
Each record has a Wingspan id, the QBO displayName, and a health of Healthy, Stale, or Error.
4. Set the default mappings
Update the connection with mappingDefaults. Every ID must come from step 3 for this same connection, or the request returns 422. This replaces V1's "default line item and default expense account" setup screen.
curl -X PATCH https://api.wingspan.app/v3/platform/accounting-connections/Qc4mTn7wKp2xRb8dLs5vHa \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mappingDefaults": {
"defaultItemId": "Op6rTw1kQn4xLm8dVs2bHc",
"expenseLedgerAccountId": "Au9dKp3wQx7tLm2nVr5sHb",
"payablePaymentLedgerAccountId": "Ro4kQw8nLx2tPm6dVb3sHa"
}
}'| Field | Used for |
|---|---|
defaultItemId | The QBO item on invoice lines Wingspan writes |
expenseLedgerAccountId | The expense account on bill lines |
payablePaymentLedgerAccountId | The bank account a bill payment is recorded against |
receivablePaymentLedgerAccountId | The account an invoice payment is recorded against |
payableClassId, receivableClassId | The QBO class for bills and invoices |
5. Map payees and payers to QBO vendors and customers
Link each Wingspan payee to its QBO vendor, and each payer to its QBO customer, using the Wingspan IDs from step 3:
curl -X PATCH https://api.wingspan.app/v3/payments/payees/Py3mQw7kLx2tRb9nVd4sHa \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "quickbooksVendorId": "Wh7pQs2nKx5tLm9dVb3rHa" }'| Record | Fields |
|---|---|
| Payee (Update a payee) | quickbooksVendorId (an AccountingVendor.id), quickbooksExpenseAccountId (a ledger account id) |
| Payer (Update a payer) | quickbooksCustomerId (an AccountingCustomer.id), quickbooksItemId (an AccountingItem.id for that payer's invoice lines) |
Match by email or name from the vendor and customer lists. A payee with no mapping gets a new QBO vendor when its first payable syncs.
What Wingspan writes to QuickBooks
These mappings describe what the sync writes today. They are carried over from V1 with V3 names.
Payable to QBO Bill
| QBO field | Wingspan source |
|---|---|
TotalAmt | Payable total |
DueDate | Payable dueDate |
Line | Payable line items (description, amount, expense account) |
VendorRef | The payee's mapped QBO vendor |
LinkedTxn | The QBO BillPayment, once the payable is paid |
Paid payable to QBO BillPayment: VendorRef (the payee's vendor), TotalAmt (payable total), PayType Check, CheckPayment.BankAccountRef (your mapped payment account), and one line linking the Bill.
New payee to QBO Vendor: PrimaryEmailAddr and DisplayName from the payee's email.
Invoice to QBO Invoice
| QBO field | Wingspan source |
|---|---|
TotalAmt | Invoice total |
DocNumber | Invoice number |
DueDate | Invoice dueDate |
Line | Invoice line items, using SalesItemLineDetail with the mapped item |
CustomerRef | The payer's mapped QBO customer |
LinkedTxn | The QBO Payment, once the invoice is paid |
Paid invoice to QBO Payment: TotalAmt, CustomerRef, ProcessPayment: true.
New payer to QBO Customer: PrimaryEmailAddr, CompanyName, DisplayName, and FullyQualifiedName from the payer's details.
Line items also accept accounting dimensions (class, glAccountCode, item, location, project, taxCode), which Wingspan stores and returns as you sent them.
Link or skip individual documents
Every invoice and payable has a quickbooks field. Use it when your own accounting system already created the QBO object, or when Wingspan shouldn't touch it:
{
"quickbooks": {
"billSync": { "id": "1841", "syncToken": "0" },
"syncControl": { "shouldCreate": false, "shouldUpdate": true, "shouldDelete": false }
}
}billSync(payables) orinvoiceSync(invoices) links the document to an existing QBO object. Send the QBOidand its currentsyncTokentogether.syncControlflags set tofalsestop Wingspan from creating, updating, or deleting the QBO object for that document.
Send it on create or on PATCH of the payable or invoice.
6. Check what synced
Every write is recorded as a sync activity. List sync activities and filter by entityType, entityId, action, or isError:
curl -G https://api.wingspan.app/v3/platform/accounting-connections/Qc4mTn7wKp2xRb8dLs5vHa/sync-activities \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
--data-urlencode "filter[isError][eq]=true"// (trimmed)
{
"data": [
{
"id": "En5tRk2wQp8xLm3nVb6dHs",
"entityType": "Payable",
"entityId": "Pb8nVx3kQm6tLw2rHd9sYc",
"action": "Create",
"partnerEntityType": "Bill",
"isError": true,
"message": "<reason reported by QuickBooks>"
}
],
"pagination": { "nextPageToken": "" }
}Fix the cause (for example, map the vendor), then retry that one row with Resync a sync activity. The connection must be Connected, or you get 409 with integrations.ConnectionNotConnected. To hide a row you've dealt with, PATCH it with { "isHidden": true }.
Keep the connection healthy
- If QuickBooks is unreachable, calls return
503withdetailCode: integrations.PartnerUnavailable. Retry later. POST .../refresh-tokenforces an OAuth token refresh.- Refresh reference data after you add vendors, customers, items, or accounts in QBO.
- If
statusbecomesDisconnected, runconnectandcomplete-connectionagain.
Webhooks
| Event | When |
|---|---|
AccountingConnection.Created, .Connected, .Disconnected, .TokenRefreshed | Connection lifecycle |
AccountingVendor.Created, .Refreshed (and the same for AccountingCustomer, AccountingItem, AccountingClass) | Reference data copied or refreshed |
Not available yet
- Starting a full sync on demand (
POST .../sync) and reading sync-run history. Use sync activities instead. - Disconnecting or deleting a connection through the V3 API. Contact support.
- Accounting partners other than QuickBooks Online.
Related pages
Updated 10 days ago