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.
2. Authorize with QuickBooks
Call Connect to get the QuickBooks authorization URL. Connecting a connection that is already Connected returns 409; disconnect it first:
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. QuickBooks redirects to a fixed Wingspan web app page (https://my.wingspan.app/member/settings/integrations/quickbooks/setup/step/1). You can't register your own redirect URI, so read code, state, and realmId from that page's URL. Pass them 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 authorization returns 422. In that 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 |
shouldAllowDelete | Whether Wingspan may send Delete sync actions to QuickBooks. Defaults to false. |
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 |
DocNumber | Payable invoice number |
TxnDate | The payable's invoice date, or else the date it was opened, or else its creation date |
PrivateNote | "View this bill in Wingspan", followed by links to the invoice and its PDFs when they exist |
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 from the payee's email. DisplayName is the payee's company name, else legal business name, else first and last name, else email. GivenName and FamilyName come from the payee's name. BillAddr is sent only when the payee has an address line 1, city, and postal code.
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, and DepositToAccountRef when a deposit account is mapped.
New payer to QBO Customer: PrimaryEmailAddr, CompanyName, and DisplayName. DisplayName is the client's company, else name, else preferred name, else email.
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, isError, isHidden, displayName (case-insensitive exact match), or a createdAt range with [gte] and [lte]. Rows are newest first. Pass sort=createdAt for oldest first:
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. To hide a row you've dealt with, PATCH it with { "isHidden": true }.
Keep the connection healthy
- If QuickBooks is unreachable, calls return
503. Retry later. - List accounting connections and Get an accounting connection show each connection's
status,syncState, andmappingDefaults. Get a sync activity reads one audit row. POST .../refresh-tokenforces an OAuth token refresh.POST .../syncrequests a full book-push sync and returns202with a sync run. List sync runs or get one to see itsstatus(Started,Completed,Failed, orPartiallyCompleted) andprogress.POST .../disconnecttears down the QuickBooks link.statusbecomesDisconnectedandAccountingConnection.Disconnectedfires. Runconnectandcomplete-connectionto reconnect.DELETE /v3/platform/accounting-connections/{connectionId}soft-deletes the connection and revokes the partner tokens.- 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 |
Partner value
This guide covers QuickBooks Online. Send partner: "Quickbooks" when you create the connection.
Related pages
Updated 11 minutes ago