Fynteq logo
Financial Automation6 min read

DATEV Integration with Stripe: A Technical Guide for German Companies

Technical guide to integrating Stripe payment data with DATEV: webhook sync, Buchungsstapel export, tax mapping, GoBD compliance, and reconciliation automation.

Fynteq Team

Summary

German companies using Stripe need automated DATEV integration for GoBD-compliant bookkeeping. This guide covers webhook-based sync, Buchungsstapel CSV generation, tax account mapping, and daily reconciliation between Stripe balance transactions and DATEV entries.

Definition

DATEV integration with Stripe connects Stripe payment events to DATEV Unternehmen online for automated bookkeeping. It maps charges, refunds, fees, and payouts to DATEV Konten (accounts), generates Buchungsstapel (batch booking entries), and maintains GoBD-compliant audit trails.

Why Manual Export Fails

Finance teams that export Stripe CSV monthly and manually create DATEV entries waste 8–15 hours per month and introduce errors. Common failures:

  • Stripe fees booked separately from gross revenue (SKR mapping errors)
  • Refunds not matched to original charge booking
  • Payout timing mismatch (Stripe payout date ≠ bank deposit date)
  • Missing USt (VAT) split on mixed-tax invoices
  • No audit trail linking DATEV entry to Stripe object ID

Automated integration eliminates these and satisfies GoBD Anwendungserlass requirements for electronic bookkeeping.

Architecture

Stripe Webhooks → Event Processor → Booking Rules Engine → DATEV Export
       ↓                ↓                    ↓                  ↓
  charge.succeeded   Normalize          SKR03 mapping     Buchungsstapel CSV
  payout.paid        Deduplicate        Tax calculation   DATEVconnect API
  charge.refunded    Validate           Fee allocation    Unternehmen online

Step 1: Kontenrahmen Mapping

Define Stripe-to-DATEV account mapping for SKR03 (most common for GmbH):

Stripe EventSoll (Debit)Haben (Credit)Amount
charge.succeeded (net)1200 (Bank)8400 (Erlöse)Net amount
charge.succeeded (USt 19%)-1776 (USt 19%)Tax amount
Stripe fee4970 (Nebenkosten)1200 (Bank)Fee amount
charge.refunded8400 (Erlöse)1200 (Bank)Refund amount
payout.paid1200 (Bank)1360 (Stripe Geldtransit)Payout amount
dispute lost4970 (Nebenkosten)1200 (Bank)Dispute amount

Store mapping in configuration, not hardcoded:

const SKR03_MAPPING = {
  charge: {
    debit: '1200',    // Bank (Stripe Transit)
    credit: '8400',   // Erlöse 19%
    taxCredit: '1776' // USt 19%
  },
  fee: {
    debit: '4970',    // Nebenkosten Geldverkehr
    credit: '1200'
  },
  refund: {
    debit: '8400',
    credit: '1200'
  },
  payout: {
    debit: '1200',    // Bank (actual bank account)
    credit: '1360'    // Stripe Geldtransit
  }
};

Adjust for SKR04 or custom Kontenrahmen with your Steuerberater.

Step 2: Webhook Event Processing

Process Stripe events into normalized booking entries:

async function processStripeEvent(event) {
  const handlers = {
    'charge.succeeded': handleChargeSucceeded,
    'charge.refunded': handleChargeRefunded,
    'charge.dispute.closed': handleDisputeClosed,
    'payout.paid': handlePayoutPaid,
    'balance_transaction.created': handleBalanceTransaction
  };

  const handler = handlers[event.type];
  if (!handler) return;

  const existing = await db.bookings.findUnique({ where: { stripeEventId: event.id } });
  if (existing) return; // Idempotent

  const bookings = await handler(event.data.object);
  await db.bookings.createMany({
    data: bookings.map(b => ({
      ...b,
      stripeEventId: event.id,
      stripeObjectId: event.data.object.id,
      status: 'pending_export'
    }))
  });
}

async function handleChargeSucceeded(charge) {
  const netAmount = charge.amount - (charge.application_fee_amount || 0);
  const taxAmount = charge.metadata?.tax_amount ? parseInt(charge.metadata.tax_amount) : 0;
  const revenueAmount = netAmount - taxAmount;

  const bookings = [
    {
      date: new Date(charge.created * 1000),
      debitAccount: SKR03_MAPPING.charge.debit,
      creditAccount: SKR03_MAPPING.charge.credit,
      amount: revenueAmount,
      currency: charge.currency.toUpperCase(),
      reference: charge.id,
      description: `Stripe Charge ${charge.id}`
    }
  ];

  if (taxAmount > 0) {
    bookings.push({
      date: new Date(charge.created * 1000),
      debitAccount: SKR03_MAPPING.charge.debit,
      creditAccount: SKR03_MAPPING.charge.taxCredit,
      amount: taxAmount,
      currency: charge.currency.toUpperCase(),
      reference: charge.id,
      description: `USt Stripe ${charge.id}`
    });
  }

  // Stripe fee (from balance transaction)
  const balanceTxn = await stripe.balanceTransactions.retrieve(charge.balance_transaction);
  if (balanceTxn.fee > 0) {
    bookings.push({
      date: new Date(charge.created * 1000),
      debitAccount: SKR03_MAPPING.fee.debit,
      creditAccount: SKR03_MAPPING.fee.credit,
      amount: balanceTxn.fee,
      currency: charge.currency.toUpperCase(),
      reference: charge.id,
      description: `Stripe Fee ${charge.id}`
    });
  }

  return bookings;
}

Step 3: Buchungsstapel Generation

Generate DATEV EXTF format (Buchungsstapel):

function generateBuchungsstapel(bookings, config) {
  const header = [
    'EXTF', '510', '21', 'Buchungsstapel', '7',
    formatDate(new Date()), '', '', '', '', '',
    config.beraterNr, config.mandantenNr,
    formatDate(config.wirtschaftsjahrBeginn), '4',
    formatDate(bookings[0].date), formatDate(bookings[bookings.length - 1].date),
    '', 'EUR', '', '', '', '', '', '', '03', '', '', '', ''
  ].join(';');

  const rows = bookings.map(b => [
    b.amount.toFixed(2).replace('.', ','),  // Umsatz
    'H',                                       // Soll/Haben (H=Haben on credit side)
    'EUR',                                     // Währung
    '',                                        // Kurs
    '',                                        // Basisumsatz
    '',                                        // WKZ
    b.amount.toFixed(2).replace('.', ','),     // Umsatz
    b.creditAccount,                           // Gegenkonto
    '',                                        // BU-Schlüssel
    formatDateDE(b.date),                      // Belegdatum
    b.reference.slice(0, 12),                  // Belegnummer
    b.description.slice(0, 60),                // Belegtext
    '1',                                       // Postensperre
    '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''
  ].join(';'));

  return [header, ...rows].join('\n');
}

function formatDateDE(date) {
  return `${String(date.getDate()).padStart(2, '0')}${String(date.getMonth() + 1).padStart(2, '0')}${date.getFullYear()}`;
}

Step 4: DATEVconnect API Upload

For automated import (requires DATEVconnect license):

async function uploadToDATEV(buchungsstapel, config) {
  const response = await fetch('https://accounting-d.datev.de/api/v1/clients/bookings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${config.datevAccessToken}`,
      'Content-Type': 'application/json',
      'X-DATEV-Client-Id': config.datevClientId
    },
    body: JSON.stringify({
      accounting_month: formatDateDE(new Date()).slice(2), // MMYYYY
      entries: parseBuchungsstapelToJSON(buchungsstapel)
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new DATEVExportError(error.message, buchungsstapel);
  }

  return response.json();
}

Alternative: Export CSV to shared folder for DATEV Unternehmen online manual import. Less automated but works without DATEVconnect.

Step 5: Payout Reconciliation

Match Stripe payouts to bank deposits:

async function reconcilePayouts() {
  const payouts = await stripe.payouts.list({ status: 'paid', limit: 100 });
  const bankTransactions = await fetchBankTransactions(last30Days);

  for (const payout of payouts.data) {
    const bankMatch = bankTransactions.find(t =>
      Math.abs(t.amount - payout.amount) < 100 && // Within €1
      Math.abs(t.date - new Date(payout.arrival_date * 1000)) < 3 * 86400000 // Within 3 days
    );

    if (bankMatch) {
      await db.reconciliation.update({
        where: { stripePayoutId: payout.id },
        data: { bankTransactionId: bankMatch.id, status: 'matched', matchedAt: new Date() }
      });
    } else {
      await alertFinance({ type: 'unmatched_payout', payoutId: payout.id, amount: payout.amount });
    }
  }
}

Step 6: GoBD Compliance

Store complete audit trail:

// Every booking entry must be traceable
const auditEntry = {
  stripeEventId: event.id,
  stripeObjectId: charge.id,
  stripeObjectType: 'charge',
  bookingEntries: bookings,
  datevExportId: exportResult?.id,
  exportedAt: new Date(),
  hash: sha256(JSON.stringify(bookings)), // Immutability proof
  createdAt: new Date()
};

await db.auditLog.create({ data: auditEntry });

GoBD requirements met:

  • Vollständigkeit: Every Stripe transaction produces a booking entry
  • Richtigkeit: Amounts match Stripe balance transactions exactly
  • Zeitgerechtheit: Daily export, not month-end batch
  • Unveränderbarkeit: Hash-verified audit log, append-only
  • Nachvollziehbarkeit: Stripe object ID in every DATEV Belegtext

Tax Handling

For Stripe Tax or manual tax calculation:

function calculateGermanTax(grossAmount, taxRate = 0.19) {
  const net = Math.round(grossAmount / (1 + taxRate));
  const tax = grossAmount - net;
  return { net, tax, gross: grossAmount };
}

// Reverse charge B2B (USt-IdNr. verified)
function isReverseCharge(customer) {
  return customer.tax_ids?.data?.some(t =>
    t.type === 'eu_vat' && t.verification.status === 'verified'
  );
}

Reverse charge bookings use BU-Schlüssel 94 (IG Erwerb) or 40 (§13b UStG) depending on service type. Confirm with Steuerberater.

Common Mistakes

  • Booking gross amounts without USt split
  • Recording Stripe fees as separate manual entries (should be automatic per charge)
  • Using payout date instead of charge date for revenue recognition
  • Missing refund-to-original-booking linkage
  • No Stripe object ID in Belegtext (breaks audit trail)
  • Monthly batch export instead of daily sync

Business Outcomes

Automated DATEV-Stripe integration saves 10–15 hours/month of manual bookkeeping, eliminates reconciliation errors at month-end, satisfies GoBD requirements, and gives Steuerberater clean Buchungsstapel files daily.

Related: E-Rechnung 2025 Guide · E-Rechnung integration

Need help connecting your finance systems?

Fynteq connects E-Rechnung, DATEV, Stripe, ERP and bank workflows for German SMEs and growing digital businesses. Frankfurt-based, fixed-scope implementation.

Frequently Asked Questions

Related articles

View all insights →

Finance integration insights

Practical guides on E-Rechnung, DATEV, Stripe, reconciliation and finance automation for teams in Germany.

By downloading, you agree to our privacy policy. We use your email to send the PDF and follow up on related services. Or contact info@fynteq.com.