ml-connector
DATEVBILL

DATEV and BILL integration

DATEV is the accounting and tax backend German firms keep their books in. BILL runs accounts payable and receivable, holding the bills, vendors, payments, and invoices a business works on day to day. Connecting the two means approved bills and the payments made against them post into DATEV as bookings, with each supporting invoice PDF filed in DATEV Unternehmen Online, all without re-keying. ml-connector handles the very different APIs on each side and submits the data on a schedule you control. Because DATEV cannot return posted journal entries through its API, BILL stays the working surface and DATEV remains the system of record.

How DATEV works

DATEV is not a conventional REST API. The cloud surface uses OAuth2 Authorization Code with PKCE through Login mit DATEV, where the tax advisor or client must consent interactively, since there is no machine-to-machine flow and access tokens last only 15 minutes. A small REST layer lists clients and uploads documents to Unternehmen Online, but actual bookings go through asynchronous file jobs: EXTF CSV files for finalized bookings to on-premise Rechnungswesen, and DXSO XML jobs for booking suggestions to Unternehmen Online. DATEV sends no webhooks, so every submitted job is confirmed by polling its status endpoint. The standard chart of accounts is not readable through the API, and finalized EXTF bookings are write-only.

How BILL works

BILL exposes accounts payable and receivable through its v3 REST API, with JSON bodies and standard HTTP verbs against gateway.prod.bill.com. Core objects include bills, vendors, payments, invoices, customers, and a readable chart of accounts, with accounting classifications such as departments, locations, and jobs for tagging line items. Authentication uses a session token, not OAuth2: a login call with a developer key plus organization credentials returns a sessionId that expires after 35 minutes of inactivity, so a 401 means re-login and retry. BILL pushes signed webhooks for events like bill.created, bill.updated, and payment.updated, verified with HMAC-SHA256, but does not support purchase orders.

What moves between them

The flow runs from BILL into DATEV. ml-connector reads approved bills and their line items, recorded payments, and the vendor records behind them from BILL, then submits them to DATEV as EXTF CSV booking batches for Rechnungswesen or DXSO XML jobs for Unternehmen Online, mapped to the matching DATEV GL accounts, tax codes, and cost centers. The invoice PDF attached to each bill is uploaded to Unternehmen Online as a Rechnungseingang document so the booking has its source receipt. Because DATEV cannot return posted journals and its chart of accounts is not readable, the mapping is one-way: ml-connector never writes ledger entries back into BILL, and account and tax-code references are configured up front rather than pulled from DATEV.

How ml-connector handles it

ml-connector stores both credential sets encrypted. On the BILL side it logs in with the developer key and organization credentials to get a sessionId, re-authenticating whenever a call returns 401 since BILL gives no explicit expiry code, and it keeps in-flight requests under BILL's limit of three concurrent calls per org. On the DATEV side it holds the OAuth2 refresh token the tax advisor granted and renews the 15-minute access token before each batch, sending only the client_id on refresh as DATEV requires, and it keeps the state value at least 20 characters on the initial consent. BILL bill.created and payment.updated webhooks, verified by HMAC-SHA256 over the minified body, act as triggers, with a scheduled poll backfilling anything a webhook missed because BILL disables a subscription after repeated delivery failures. GL accounts, tax codes, and cost centers are mapped first, since DATEV will not return its chart of accounts, so every EXTF or DXSO line lands on a valid account. Each booking job is submitted, then polled with exponential backoff until DATEV reports complete or failed, because there is no synchronous confirmation. EXTF files use stable deterministic filenames and NFC-normalized UTF-8 so a retry is duplicate-safe rather than rejected with DATEV error DCO01253. Every record carries a full audit trail and can be replayed if a downstream call fails.

A real-world example

A 60-person facilities services company in Germany runs its books with a DATEV tax advisor but manages day-to-day accounts payable in BILL, where managers approve vendor bills and schedule payments. Before the integration, a bookkeeper exported approved bills each week and re-typed them into a DATEV EXTF file by hand, matched each one to a GL account and tax code, and emailed scanned invoices to the advisor separately, so postings lagged and the advisor often chased missing receipts at month-end. With DATEV and BILL connected, each approved bill and recorded payment is submitted to DATEV as a booking job within the polling window, the invoice PDF is filed in Unternehmen Online against the same entry, and the line is allocated to the correct account and cost center. The weekly re-keying disappears and the advisor closes the period with the receipts already attached.

What you can do

  • Submit approved BILL bills and recorded payments into DATEV as EXTF CSV or DXSO XML booking jobs.
  • Upload each BILL invoice PDF to DATEV Unternehmen Online as a Rechnungseingang document tied to the booking.
  • Map BILL line items to DATEV GL accounts, tax codes, and cost centers configured up front, since DATEV will not return its chart of accounts.
  • Bridge BILL session-token login and the DATEV OAuth2 PKCE consent the tax advisor grants, refreshing each token before it expires.
  • Trigger on BILL webhooks, poll each DATEV job to confirm posting, and replay any failed record from a full audit trail.

Questions

Which direction does data move between DATEV and BILL?
The flow is one-way, from BILL into DATEV. Approved bills, recorded payments, vendor data, and invoice PDFs move from BILL into DATEV as booking jobs and document uploads. DATEV cannot return posted journal entries through its API and its chart of accounts is not readable, so ml-connector never writes ledger data back into BILL and account references are configured in advance.
How does the integration handle DATEV's lack of webhooks and asynchronous bookings?
DATEV sends no webhooks and processes bookings as asynchronous file jobs, so there is no instant confirmation that an entry posted. ml-connector submits each EXTF or DXSO job, receives a job ID, and polls the status endpoint with exponential backoff until DATEV reports complete or failed. On the BILL side it uses bill.created and payment.updated webhooks as triggers, with a scheduled poll to backfill anything a webhook missed.
How are the two different logins reconciled?
BILL uses a session token from a developer-key login that expires after about 35 minutes of inactivity, while DATEV uses OAuth2 Authorization Code with PKCE that the tax advisor must consent to interactively, with a 15-minute access token. ml-connector stores both encrypted, re-logs into BILL whenever a call returns 401, and refreshes the DATEV token before each batch using the client_id only, as DATEV requires on refresh.

Related integrations

Connect DATEV and BILL

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

Get started