ml-connector
DATEVHubSpot

DATEV and HubSpot integration

DATEV runs accounting and tax for German and Austrian businesses through its tax advisors. HubSpot runs the CRM, where invoices, payments, contacts, and companies live. Connecting the two means invoices raised in HubSpot become bookings and uploaded documents in DATEV without re-keying, and the customers behind those invoices land as creditor and debtor master data. ml-connector handles the very different surfaces on each side, since DATEV bookings are asynchronous file jobs while HubSpot is a straightforward REST CRM. Because neither side exposes a readable chart of accounts, the general ledger stays in DATEV where it belongs.

How DATEV works

DATEV is not a single REST API. The accounting:clients and accounting:documents products are REST, used to look up the DATEV client id and to upload invoice PDFs to DATEV Unternehmen Online. Finalized bookings are submitted as asynchronous jobs: EXTF CSV files to the on-premise DATEV Rechnungswesen engine, or DXSO XML jobs to DATEV Unternehmen Online, each returning a job id you poll until complete. Authentication is OAuth 2.0 Authorization Code with PKCE through login.datev.de, so a real tax-advisor user session is always required and access tokens expire after 15 minutes. DATEV has no webhooks, the chart of accounts is not readable through the API, and EXTF bookings are write-only.

How HubSpot works

HubSpot exposes its CRM over a REST JSON API at api.hubapi.com, covering contacts, companies, deals, invoices, line items, products, and payments. Server-to-server access uses a Private App access token starting with pat- passed as a Bearer header, scoped by the permissions chosen when the app is created. Reads page with a cursor using limit and after, and batch upsert keyed on an id property gives safe, duplicate-free re-runs. HubSpot can push record events by webhook, but only from a Public OAuth app, so a Private App token integration reads on a schedule unless a separate OAuth app is registered for events.

What moves between them

The flow runs one direction, from HubSpot into DATEV. ml-connector reads invoices and their line items from HubSpot, along with the associated company and contact, and submits them into DATEV as booking jobs: an EXTF CSV booking batch for the finalized entry, or a DXSO XML job to DATEV Unternehmen Online for a booking suggestion. The invoice PDF is uploaded to DATEV Unternehmen Online through the documents API against the right client-specific document type. Companies and contacts are written into the EXTF master data file as creditor and debtor records. Nothing financial flows back from DATEV, because EXTF bookings are write-only and DATEV exposes no posted journals or GL accounts to read.

How ml-connector handles it

ml-connector stores both credential sets encrypted. On the HubSpot side it sends the Private App token as a Bearer header and pages invoices and line items with limit and after. On the DATEV side it runs the OAuth 2.0 PKCE login once with the tax advisor, then refreshes the 15-minute access token automatically, sending only the client id on refresh as DATEV requires. It first calls accounting:clients to resolve the DATEV client id used as a path parameter on every other call, and fetches the client-specific document types before any PDF upload rather than hardcoding them. HubSpot GL accounts and tax codes do not exist, so the DATEV account and Steuerschluessel for each booking line come from a mapping you configure once, since DATEV will not return its chart of accounts. EXTF CSV is written as NFC-normalized UTF-8 with a whitelisted filename, because non-precomposed characters are silently rejected. DATEV processing is asynchronous and gives no webhook, so ml-connector polls each job id with exponential backoff and jitter until it reports complete or failed. Duplicate submissions are avoided by generating stable, deterministic EXTF filenames, which DATEV treats as the dedup key, alongside a BullMQ job id, so a re-read HubSpot invoice is never booked twice. Document uploads use the idempotent PUT with a supplied GUID over POST. Every record carries a full audit trail and can be replayed if a job fails.

A real-world example

A German digital agency with around 60 staff runs HubSpot as its CRM and raises client invoices there, while its external tax advisor keeps the books in DATEV. Before the integration, an office manager exported each HubSpot invoice and emailed the PDFs to the advisor, who re-keyed the bookings into DATEV and chased mismatched customer names and missing receipts during every monthly close. With DATEV and HubSpot connected, each HubSpot invoice is submitted as a DATEV booking job and its PDF uploaded to DATEV Unternehmen Online within the sync window, with the customer written as a debtor record and the line mapped to the agreed GL account. The advisor opens a clean booking batch instead of a folder of emails, and the manual re-keying step is gone.

What you can do

  • Submit HubSpot invoices and line items into DATEV as EXTF CSV booking batches or DXSO XML jobs.
  • Upload HubSpot invoice PDFs to DATEV Unternehmen Online against the correct client-specific document type.
  • Write HubSpot companies and contacts into DATEV as creditor and debtor master data.
  • Bridge the HubSpot Private App Bearer token and DATEV's OAuth 2.0 PKCE login with automatic token refresh.
  • Poll each DATEV job to confirmation with stable-filename dedup, retries, and a full audit trail on every record.

Questions

Which direction does data move between DATEV and HubSpot?
The flow is one direction, from HubSpot into DATEV. Invoices, line items, and the companies and contacts behind them move from HubSpot into DATEV as booking jobs and uploaded documents. DATEV bookings submitted as EXTF files are write-only and DATEV exposes no readable chart of accounts, so ml-connector never writes financial data back into HubSpot.
How does ml-connector handle DATEV having no webhooks?
DATEV does not push any events, and all booking and document imports are asynchronous. After ml-connector submits an EXTF or DXSO job it receives a job id and polls the DATEV status endpoint with exponential backoff and jitter, from about 5 seconds up to a 60-second cap, until the job reports complete or failed. The HubSpot side is read on a schedule the same way, since a Private App token cannot receive HubSpot webhooks.
What maps to what, given DATEV does not expose GL accounts?
HubSpot invoices and line items map to DATEV booking rows, and HubSpot companies and contacts map to DATEV creditor and debtor master data in the EXTF file. Because DATEV will not return its chart of accounts through the API and HubSpot has no GL accounts at all, the DATEV account number and tax code for each line come from a mapping you configure once. ml-connector validates that mapping before submitting a job so postings land on valid accounts.

Related integrations

Connect DATEV and HubSpot

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

Get started