ml-connector
Wave AccountingBILL

Wave Accounting and BILL integration

Wave Accounting handles invoicing, expense tracking, and the general ledger for small businesses. BILL runs the accounts payable workflow, where bills are entered, approved, and paid to vendors. Connecting the two lets vendors and GL accounts defined in Wave drive how bills are coded in BILL, and lets every payment BILL makes flow back into Wave's books as a journal entry. Wave has no structured bill or payment object, so BILL becomes the AP system of record while Wave stays the ledger. ml-connector handles the different APIs and authentication models on each side and moves the data on a schedule you control.

How Wave Accounting works

Wave Accounting exposes customers, invoices, products, vendors, chart-of-accounts entries, and money transactions through a single GraphQL endpoint at gql.waveapps.com, where reads are queries and writes are mutations. It authorizes with OAuth2 using the authorization code flow, returns access tokens that expire in about two hours, and issues refresh tokens when the offline_access scope is granted. Every query and mutation is scoped to a business id that must be fetched first. Wave can push webhooks for events such as invoice, payment, customer, and transaction changes, signed with HMAC-SHA256 in an x-wave-signature header. It has no purchase order object and no structured AP or bill workflow.

How BILL works

BILL exposes vendors, bills, payments, AR invoices, customers, and chart-of-accounts entries through a REST v3 API at gateway.prod.bill.com, using standard HTTP verbs and JSON. Authentication is a session-token model: a login call with a developer key, organization id, username, and password returns a sessionId used on every request, and that session expires after 35 minutes of inactivity. Pagination is offset based with a maximum of 100 records per page, and only three concurrent requests are allowed per organization. BILL can push webhooks signed with HMAC-SHA256 in an x-bill-sha-signature header, and it has no purchase order object since it is AP and AR focused.

What moves between them

The flow runs in both directions but with distinct records. Vendors and chart-of-accounts entries move from Wave Accounting into BILL, so AP bills entered in BILL reference the same vendor list and GL accounts the business already keeps in Wave. Each completed BILL payment then moves back into Wave as a money transaction, posted against the funding and expense accounts, because Wave has no native payment object to receive it directly. Customers and AR invoices can also sync from Wave into BILL where the business uses BILL for receivables. Cadence is event driven where BILL and Wave webhooks fire, with a scheduled poll as the backstop.

How ml-connector handles it

ml-connector stores both credential sets encrypted. On the Wave side it holds the OAuth2 client id, secret, and redirect uri, fetches the business id once after authorization, and refreshes the access token proactively before the two-hour expiry rather than waiting for a 401. On the BILL side it logs in with the developer key, organization id, username, and password to obtain a sessionId, and re-logs in and retries whenever a call returns an auth error, since BILL signals an expired session as a generic 401. Wave vendors map to BILL vendors and Wave accounts map to BILL chart-of-accounts entries, so bill line items code to valid GL accounts. Returning BILL payments are written into Wave with moneyTransactionCreate, and the externalId field is set to a stable key derived from the BILL payment id, which prevents a duplicate journal entry if a delivery is retried. BILL allows only three concurrent requests per organization, so ml-connector queues calls and backs off on the BDC_1144 hourly limit and BDC_1322 concurrency errors. Webhook payloads are verified against each system's HMAC-SHA256 signature before processing, using the raw Wave body and the minified BILL body.

A real-world example

A 30-person specialty construction subcontractor keeps its books in Wave Accounting because the invoicing and ledger are free, but it pays dozens of material suppliers each month and needs approvals and ACH payments that Wave does not provide, so it runs accounts payable in BILL. Before the integration, a bookkeeper maintained the vendor list and account codes in both tools by hand, then re-entered every BILL payment into Wave as a manual transaction at month-end to keep the ledger accurate. With Wave Accounting and BILL connected, vendors and GL accounts stay aligned automatically, and each payment BILL completes posts itself into Wave coded to the right accounts. The duplicate data entry is gone and the ledger reflects AP activity without a month-end catch-up.

What you can do

  • Push Wave Accounting vendors and chart-of-accounts entries into BILL so AP bills code to the correct GL accounts.
  • Write each completed BILL payment back into Wave as a money transaction against the right funding and expense accounts.
  • Sync Wave customers and AR invoices into BILL where the business uses BILL for receivables.
  • Bridge Wave OAuth2 bearer tokens and the BILL devKey session login, refreshing each before it expires.
  • Verify Wave and BILL webhooks by HMAC-SHA256 and key Wave transactions by externalId to prevent duplicate postings.

Questions

Which direction does data move between Wave Accounting and BILL?
It moves both ways with different records. Vendors and GL accounts flow from Wave into BILL so bills are coded consistently, and completed BILL payments flow back into Wave as journal-entry transactions. Wave has no native bill or payment object, so BILL stays the system of record for accounts payable while Wave stays the ledger.
How does the integration handle BILL session expiry and Wave token expiry?
ml-connector logs into BILL with the developer key and org credentials to get a sessionId, and because BILL reports an expired session as a generic 401, it re-logs in and retries the call automatically. On the Wave side it refreshes the OAuth2 access token before the roughly two-hour expiry using the offline_access refresh token, so a sync does not fail mid-run.
How are duplicate payments and bills prevented?
BILL does not offer a native idempotency key, so ml-connector checks existing records by their BILL ids before creating anything. When writing a BILL payment into Wave, it sets the transaction externalId to a stable key derived from the BILL payment id, which Wave uses to reject a second create if a webhook or job is retried.

Related integrations

Connect Wave Accounting and BILL

Free to use. Add your credentials, ping your real systems, and see if we fit.

Get started