ml-connector
DATEVExpensify

DATEV and Expensify integration

DATEV is the accounting backend used by German tax advisors and their business clients. Expensify is where employees capture expenses and managers approve them. Connecting the two moves approved expense spend into the books without re-keying. After a report is approved or reimbursed in Expensify, ml-connector reads its expense lines, writes them into DATEV as bookings, and uploads the receipt images to DATEV Unternehmen Online. ml-connector handles the very different APIs on each side and runs the sync on a schedule you set.

How DATEV works

DATEV's accounting cloud is not a conventional REST API. The accounting:clients product reads the companies the logged-in user can access, accounting:documents uploads receipt and invoice files into DATEV Unternehmen Online, and bookings are submitted as asynchronous jobs through accounting:extf-files (EXTF CSV to the on-premise Rechnungswesen engine) or accounting:dxso-jobs (DXSO XML to DUO). Every call uses OAuth2 Authorization Code with PKCE against login.datev.de, requires a real DATEV user session granted by the tax advisor, and carries a Bearer token plus an X-DATEV-Client-Id header. DATEV has no webhooks, so job status is read by polling, and finalized EXTF bookings are write-only.

How Expensify works

Expensify's Integration Server API sends every request as an HTTP POST to a single URL, with the operation chosen by the type and dataType fields in a JSON requestJobDescription body. It authenticates with a long-lived partnerUserID and partnerUserSecret pair passed inline on each call rather than OAuth tokens. It exposes expense reports, individual expense lines (merchant, amount in cents, category, tag, billable, reimbursable, externalID), policies, and corporate card reconciliation. Expensify does not push webhooks; the onReceive and onFinish hooks run inside an export job, so the connector polls and filters by approvedAfter to pick up only new reports.

What moves between them

The flow runs from Expensify into DATEV. ml-connector polls Expensify for reports that reach the APPROVED or REIMBURSED state, exports them as JSON, and for each expense line writes a DATEV booking: the category GL code becomes the DATEV account (Konto and Gegenkonto), the amount in cents is converted and given a debit or credit indicator, and the tag becomes the cost center (Kostenstelle). Bookings go in as EXTF CSV jobs to Rechnungswesen or DXSO XML jobs to DUO, and each receipt PDF is uploaded through accounting:documents as an incoming-invoice document. DATEV is a write target here: posted EXTF entries and the DATEV chart of accounts cannot be read back, so no financial data flows from DATEV into Expensify.

How ml-connector handles it

ml-connector stores both credential sets encrypted. It runs the DATEV OAuth2 PKCE flow with code_challenge_method S256, a state of at least twenty characters, and a nonce, refreshing the fifteen-minute access token with the client_id only, and it sends the partnerUserID and partnerUserSecret inline on every Expensify POST. Because neither system sends webhooks, it polls Expensify with approvedAfter and a markedAsExported filter so each report is ingested once, then calls markAsExported after a successful post. Expensify category GL codes are mapped to DATEV SKR account numbers and tags to cost centers up front, since the DATEV chart of accounts is not readable through the API and must be configured by hand. Receipt uploads first fetch the client-specific document types, because they differ per DATEV client, and prefer the idempotent PUT with a GUID. EXTF files are written as UTF-8 with precomposed characters and deterministic filenames so DATEV's duplicate check, error DCO01253, makes a retry safe. Every booking job is submitted and then polled to completion with backoff, since processing is asynchronous, and Expensify rate limits of five requests per ten seconds and twenty per minute are respected with backoff and jitter. Every record carries a full audit trail and can be replayed if a DATEV job fails.

A real-world example

A German engineering services firm of about 180 staff runs its books in DATEV through an external Steuerberater and uses Expensify so consultants can photograph receipts and submit travel claims from client sites. Before the integration, the bookkeeper exported approved reports from Expensify each week, hand-keyed the totals into DATEV against the right expense accounts and cost centers, and uploaded receipt PDFs one by one into DATEV Unternehmen Online. With DATEV and Expensify connected, every approved report posts into DATEV as a booking allocated to the correct account and cost center, and its receipts land in DUO automatically. The weekly re-keying is gone, the tax advisor sees the documents without chasing them, and the month closes with travel spend already in the ledger.

What you can do

  • Post approved and reimbursed Expensify expense reports into DATEV as EXTF CSV or DXSO XML booking jobs.
  • Upload each Expensify receipt PDF into DATEV Unternehmen Online as an incoming-invoice document.
  • Map Expensify category GL codes to DATEV accounts and Expensify tags to DATEV cost centers.
  • Bridge the Expensify partner key pair and the DATEV OAuth2 PKCE user login, refreshing the short-lived DATEV token automatically.
  • Poll Expensify on a schedule with markedAsExported dedup, retries, and a full audit trail on every record.

Questions

Which direction does data move between DATEV and Expensify?
The flow is Expensify into DATEV. Approved and reimbursed expense reports and their receipt images move from Expensify into DATEV as bookings and uploaded documents. Finalized DATEV EXTF entries are write-only and the DATEV chart of accounts is not readable through the API, so no financial data flows back into Expensify.
How are expense categories matched to DATEV accounts?
Expensify attaches a GL code to each category through its categoryGLCodeMap, and tags act as accounting dimensions. ml-connector maps those GL codes to DATEV account numbers (Konto and Gegenkonto) and tags to cost centers up front. This mapping is configured manually because DATEV does not expose its chart of accounts through the API.
Do either of these systems send webhooks, or does the connector poll?
Neither sends webhooks. Expensify's onReceive and onFinish hooks run inside an export job rather than pushing events, and DATEV has no event notifications at all. ml-connector polls Expensify for newly approved reports using approvedAfter and polls each DATEV booking job to completion, since DATEV processes file imports asynchronously.

Related integrations

Connect DATEV and Expensify

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

Get started