Dieser Guide erklärt den praktischen Einsatz der kassensichv-Bibliothek für die gesetzeskonforme TSE-Anbindung nach KassenSichV, BSI TR-03153 und DSFinV-K 2.3.
→ kassensichv (Unit-Referenz) · kassensichv.manager · Mock · REST · USB
Die Kassensicherungsverordnung (KassenSichV) verpflichtet alle Kassensysteme in Deutschland seit dem 1. Januar 2020, jeden Kassenvorgang mit einer zertifizierten TSE (Technische Sicherheitseinrichtung) zu signieren. Jeder Bon muss diese Pflichtangaben enthalten:
Der QR-Code-String hat exakt dieses Format:
V0;{TSE-Seriennummer};{StartUTC};{EndUTC};{Signaturzähler};{AnzahlTransaktionen};{Signaturwert}
Die kassensichv-Bibliothek erzeugt diesen String automatisch in SigErgebnis.qrCode — kein manuelles Formatieren nötig.
Das zentrale Konzept der Bibliothek: Die gesamte Kassensoftware interagiert nur mit dem TseManager. Welche TSE physisch angebunden ist, bestimmt ausschließlich der Provider beim Start:
| Provider | Unit | Typischer Einsatz |
|---|---|---|
TseMockNew() | kassensichv.mock | Tests, CI/CD, Bondruckvorschau — keine Hardware nötig |
TseRestNew(cfg) | kassensichv.rest | Cloud-TSE: Fiskaly, Deutsche Fiskal — billiger Einstieg |
TseFileNew(cfg) | kassensichv.file | USB-TSE: Swissbit, Epson — stationäre Kasse mit eigener Hardware |
Wechsel zwischen Providern: Nur die TseXxxNew-Zeile ändern. Der gesamte Beleg-Code bleibt identisch — das ist der Hauptvorteil der Dependency-Inversion-Architektur.
Während der Entwicklung und in CI/CD-Pipelines immer den Mock-Provider verwenden. Er benötigt keine Hardware, keine Netzwerkverbindung und keine API-Keys.
import kassensichv.types;
import kassensichv.mock;
import kassensichv.manager;
fn main(): int64 {
var mgr: int64 := TseMockNew();
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Kaffee;2.50_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 250; // Eurocent (kein float — Rundungsfehler!)
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
// Mock-Ausgabe:
// V0;MOCK-TSE-0000000000000001;2026-06-12T09:30:00Z;2026-06-12T09:30:01Z;1;1;base64...
PrintLn(s.qrCode as pchar);
SigErgebnisFree(sig);
TseManagerFree(mgr);
return 0;
}
Der Mock kann alle Fehlerzustände simulieren — ohne echte TSE-Hardware:
// Verbindungsabbruch simulieren (code=503)
TseMockSetSimulateError(mgr, 1);
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
// s.success == 0, s.errorMsg enthält "Simulierter Verbindungsfehler"
SigErgebnisFree(sig);
TseMockSetSimulateError(mgr, 0);
// Timeout simulieren (code=408)
TseMockSetSimulateTimeout(mgr, 1);
// ... gleiche Struktur ...
TseMockSetSimulateTimeout(mgr, 0);
Für Cloud-TSE-Anbieter (Fiskaly, Deutsche Fiskal). Vorteil: Keine Hardware, zentrale Verwaltung, für mobile Kassen und Online-Shops geeignet.
import kassensichv.types;
import kassensichv.rest;
import kassensichv.manager;
fn ManagerFuerFiskaly(): int64 {
// Konfiguration als JSON-String (in Produktion aus env/config laden)
var cfg: pchar := concat(
"{\"api_url\":\"https://kassensichv.io/api/v1\","c,
"\"api_key\":\""c, EnvGet("FISKALY_API_KEY"c), "\","c,
"\"client_id\":\""c, EnvGet("KASSEN_ID"c), "\"}"c
);
return TseRestNew(cfg as int64);
}
Empfohlene Umgebungsvariablen:
| Variable | Bedeutung |
|---|---|
FISKALY_API_KEY | Bearer-Token (nie im Code hardcoden!) |
KASSEN_ID | Kassen-ID beim Cloud-Anbieter |
Der REST-Provider wiederholt Requests automatisch bei HTTP 503/504 (bis zu 3 Versuche, exponentielles Backoff: 1s → 2s → 4s). TLS 1.2 wird erzwungen.
Für stationäre Kassen mit USB-TSE-Stick (Swissbit, Epson). Der Stick muss als Dateisystem gemountet sein.
import kassensichv.types;
import kassensichv.file;
import kassensichv.manager;
fn ManagerFuerSwissbit(mountPunkt: pchar): int64 {
// "{\"base_path\":\"/mnt/tse\",\"timeout_ms\":5000}"
var cfg: pchar := concat(
"{\"base_path\":\""c, mountPunkt, "\","c,
"\"timeout_ms\":5000}"c
);
return TseFileNew(cfg as int64);
}
Vorbereitung unter Linux:
sudo mount /dev/sdb1 /mnt/tse
Oder dauerhaft via /etc/fstab:
UUID=xxxx-yyyy /mnt/tse vfat auto,user,rw 0 0
Der Standardfall für eine einzelne Transaktion (Bezahlung an der Kasse):
import kassensichv.types;
import kassensichv.mock;
import kassensichv.manager;
fn EinfacherKassenbon(mgr: int64): void {
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Laptop;999.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 99900; // 999,00 EUR in Cent
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 0 then {
Print("[FEHLER] "c); PrintLn(s.errorMsg as pchar);
SigErgebnisFree(sig);
return;
}
// Pflichtfelder auf Bon ausgeben
Print("TSE-Seriennummer: "c); PrintLn(s.tseSerial as pchar);
Print("Signaturzähler: "c); PrintLn(IntToStr(s.sigZaehler)c);
Print("Signaturwert: "c); PrintLn(s.sigWert as pchar);
Print("QR-Code: "c); PrintLn(s.qrCode as pchar);
SigErgebnisFree(sig);
}
Für Restaurants, Servicebetriebe und andere Vorgänge mit Zwischenständen:
fn Tischbestellung(mgr: int64, tischNr: int64): void {
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "KASSE-001"c;
// Transaktion öffnen — TSE vergiBt Zähler + Startzeitpunkt
var transId: int64 := TseOpenBeleg(mgr, addr beleg);
// Vorspeise geliefert (Zwischenstand, optional)
beleg.prozessDaten := "Suppe;5.50_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 550;
var zwi: int64 := TseUpdateBeleg(mgr, transId, addr beleg);
SigErgebnisFree(zwi);
// Hauptgang hinzu
beleg.prozessDaten := "Suppe+Schnitzel;22.50_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 2250;
var zwi2: int64 := TseUpdateBeleg(mgr, transId, addr beleg);
SigErgebnisFree(zwi2);
// Rechnung — Transaktion abschließen
beleg.prozessDaten := "Suppe+Schnitzel+Dessert;30.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 3000;
var sig: int64 := TseCloseBeleg(mgr, transId, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 1 then {
PrintLn(s.qrCode as pchar); // QR-Code auf Bon
} else {
// COMPLIANCE-WARNUNG: Transaktion möglicherweise offen!
Print("[COMPLIANCE] "c); PrintLn(s.errorMsg as pchar);
}
SigErgebnisFree(sig);
free(transId);
}
Ein Stornobon ist ein eigenständiger Kassenbon mit dem Prozesstyp PROZESSTYP_STORNO. Er wird genauso signiert wie ein normaler Bon — nur der Prozesstyp und die Belegdaten unterscheiden sich:
fn Stornierung(mgr: int64, originalBetrag: int64): void {
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_STORNO;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Storno Laptop;-999.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := -99900; // negativer Betrag
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 1 then {
PrintLn(s.qrCode as pchar);
}
SigErgebnisFree(sig);
}
Testkäufe und Mitarbeiterschulungen müssen mit dem Prozesstyp PROZESSTYP_TRAINING signiert werden. Diese Vorgänge sind steuerlich nicht relevant, aber trotzdem TSE-pflichtig:
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_TRAINING;
beleg.kassenNr := "KASSE-001"c;
beleg.prozessDaten := "Training;0.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 0;
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
// ... Bon als "TRAINING" markieren, nicht als echter Kassenbon
Finanzämter können den Export der TSE-Audit-Logs anfordern. Die Bibliothek erzeugt:
{path}/tse_export.tar — TSE-intern signiertes Archiv (nicht verändern!){path}/index.json — Exportmetadaten nach DSFinV-K 2.3
fn FinanzamtExport(mgr: int64): int64 {
var exportPfad: pchar := "/var/kassensichv/export"c;
var ok: int64 := TseExportAuditData(mgr, exportPfad as int64, "KASSE-001"c);
if ok == 0 then {
PrintLn("Export fehlgeschlagen — Verzeichnis nicht beschreibbar?"c);
return 0;
}
// Ergebnis: /var/kassensichv/export/tse_export.tar + index.json
PrintLn("DSFinV-K-Export erfolgreich"c);
return 1;
}
index.json Inhalt:
{
"dsfinvk_version": "2.3",
"tse_serial": "SWB-0123456789ABCDEF",
"kasse_id": "KASSE-001",
"export_timestamp": "2026-06-12T09:00:00Z"
}
Alle Fehler werden über SigErgebnis.success und SigErgebnis.errorMsg zurückgegeben:
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 0 then {
Print("TSE-Fehler: "c);
PrintLn(s.errorMsg as pchar);
SigErgebnisFree(sig);
return 0;
}
// Bon drucken ...
SigErgebnisFree(sig);
Wenn TseCloseBeleg mit success=0 zurückkommt, ist die TSE-Transaktion möglicherweise noch offen. Das ist ein Compliance-Problem — die Kassensoftware muss reagieren:
var sig: int64 := TseCloseBeleg(mgr, transId, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.success == 0 then {
// PFLICHT: Vorfall protokollieren
Print("[COMPLIANCE] Offene TSE-Transaktion — TransId: "c);
PrintLn(transId as pchar);
Print("[COMPLIANCE] Fehler: "c);
PrintLn(s.errorMsg as pchar);
// TSE-Status prüfen
var status: int64 := TseGetStatus(mgr);
Print("[COMPLIANCE] TSE-Status: "c);
PrintLn(status as pchar);
free(status);
// Bon NICHT drucken bis Situation geklärt
SigErgebnisFree(sig);
free(transId);
return 0;
}
Faustregel: Nie einen Kassenbon ausgeben, wenn success=0. Die Kassensoftware muss die offene Transaktion manuell abschließen oder beim TSE-Anbieter melden.
| Code | Bedeutung | Empfohlene Reaktion |
|---|---|---|
| 400 | Konfigurationsfehler | Programm nicht starten, Konfiguration prüfen |
| 403 | Exportpfad nicht schreibbar | Verzeichnis anlegen, Rechte prüfen |
| 408 | Timeout | Verbindung zur TSE prüfen, USB-Stick eingesteckt? |
| 409 | Offene Transaktion | Vorfall protokollieren, Support kontaktieren |
| 500 | TSE-interner Signierfehler | TSE-Hardware defekt? TSE-Anbieter kontaktieren |
| 503 | Verbindungsfehler | Netzwerk (REST) oder USB-Mount (File) prüfen |
Konfigurationsfehler möglichst früh erkennen — nicht erst beim ersten Bon:
import kassensichv.rest;
import kassensichv.manager;
fn TseInitPruefen(mgr: int64): int64 {
// Status als erstes abfragen — erkennt Konfigurationsfehler sofort
var status: int64 := TseGetStatus(mgr);
if status == 0 then {
PrintLn("[FEHLER] TSE konnte nicht initialisiert werden"c);
return 0;
}
// Seriennummer prüfen — bei REST: wird von API geladen
var serial: int64 := TseGetSerial(mgr);
if serial == 0 then {
PrintLn("[FEHLER] TSE-Seriennummer nicht abrufbar"c);
free(status);
return 0;
}
Print("TSE bereit: "c); PrintLn(serial as pchar);
free(serial);
free(status);
return 1;
}
fn main(): int64 {
var mgr: int64 := TseRestNew(cfg as int64);
if TseInitPruefen(mgr) == 0 then {
TseManagerFree(mgr);
return 1; // Programm abbrechen
}
// Kassensoftware starten ...
return 0;
}
Niemals f64 für Geldbeträge verwenden. IEEE-754-Gleitkommazahlen haben Rundungsfehler (0.1 + 0.2 ≠ 0.3). Das umsatz-Feld in BelegDaten ist int64 in Eurocent:
// FALSCH — Rundungsfehler bei 0.1 + 0.2
var betrag: f64 := 9.99;
beleg.umsatz := betrag as int64; // kann 998 oder 999 ergeben!
// RICHTIG — integer Eurocent
beleg.umsatz := 999; // 9,99 EUR = 999 Cent
// RICHTIG — Addition
var teilbetrag1: int64 := 550; // 5,50 EUR
var teilbetrag2: int64 := 800; // 8,00 EUR
beleg.umsatz := teilbetrag1 + teilbetrag2; // exakt 1350 Cent = 13,50 EUR
Das DSFinV-K-Format erwartet Beträge mit Punkt als Dezimaltrennzeichen (9.99) im prozessDaten-String — das ist reines Textformat und unabhängig von der internen Cent-Rechnung.
Das prozessDaten-Feld folgt dem DSFinV-K 2.3-Schema. Für einfache Kassenbons:
{Artikel};{Betrag_19%}_{Betrag_7%}_{Betrag_0%}_{Betrag_special}_{Betrag_sonstig}
Mehrwertsteuer-Zuordnung nach Steuersatz:
| Spalte | Steuersatz | Typischer Einsatz |
|---|---|---|
| Feld 1 (19%) | Normaler MwSt.-Satz | Elektronik, Kleidung, Haushaltsware |
| Feld 2 (7%) | Ermäßigter MwSt.-Satz | Lebensmittel, Bücher, ÖPNV-Tickets |
| Feld 3 (0%) | Steuerfreie Umsätze | Exportlieferungen, Versicherungen |
| Feld 4 | Besonderer Steuersatz | Spezialfälle |
| Feld 5 | Sonstige | Trinkgelder, Gutscheineinlösung |
// Laptop (19% MwSt.): 999,00 EUR
beleg.prozessDaten := "Laptop;999.00_0.00_0.00_0.00_0.00"c;
// Buch (7% MwSt.): 15,90 EUR
beleg.prozessDaten := "Buch;0.00_15.90_0.00_0.00_0.00"c;
// Gemischter Einkauf
beleg.prozessDaten := "Laptop+Buch;999.00_15.90_0.00_0.00_0.00"c;
// Restaurant-Bestellung (Speisen 7%, Getränke 19%)
beleg.prozessDaten := "Getränke+Speisen;8.50_12.00_0.00_0.00_0.00"c;
Für vollständige DSFinV-K-Anforderungen (komplexe Bonpflicht-Details, Kassenabschlüsse) die offizielle DSFinV-K 2.3-Spezifikation des BMF (Bundesministerium der Finanzen) konsultieren.
import kassensichv.types;
import kassensichv.mock;
import kassensichv.manager;
fn TestEinfacherBon(): int64 {
var mgr: int64 := TseMockNew();
var beleg: BelegDaten;
beleg.prozessTyp := PROZESSTYP_KASSENBELEG;
beleg.kassenNr := "TEST-001"c;
beleg.prozessDaten := "Test-Artikel;1.00_0.00_0.00_0.00_0.00"c;
beleg.umsatz := 100;
var sig: int64 := TseProcessBeleg(mgr, addr beleg);
var s: SigErgebnis := (sig as *SigErgebnis)^;
var ok: int64 := 1;
// success muss 1 sein
if s.success == 0 then { PrintLn("FAIL: success=0"c); ok := 0; }
// Zähler muss 1 sein (erster Bon)
if s.sigZaehler != 1 then { PrintLn("FAIL: sigZaehler != 1"c); ok := 0; }
// QR-Code muss mit V0;MOCK beginnen
// (Vergleich mit StringStartsWith aus std.string)
SigErgebnisFree(sig);
TseManagerFree(mgr);
return ok;
}
fn TestSignaturZaehlerMonoton(): int64 {
var mgr: int64 := TseMockNew();
var prevZaehler: int64 := 0;
var i: int64 := 1;
while i <= 5 do {
var b: BelegDaten;
b.prozessTyp := PROZESSTYP_KASSENBELEG;
var sig: int64 := TseProcessBeleg(mgr, addr b);
var s: SigErgebnis := (sig as *SigErgebnis)^;
if s.sigZaehler <= prevZaehler then {
PrintLn("FAIL: Zähler nicht monoton steigend"c);
SigErgebnisFree(sig);
TseManagerFree(mgr);
return 0;
}
prevZaehler := s.sigZaehler;
SigErgebnisFree(sig);
i := i + 1;
}
TseManagerFree(mgr);
return 1;
}
fn TestTimeout(): int64 {
var mgr: int64 := TseMockNew();
TseMockSetSimulateTimeout(mgr, 1);
var b: BelegDaten; b.prozessTyp := PROZESSTYP_KASSENBELEG;
var sig: int64 := TseProcessBeleg(mgr, addr b);
var s: SigErgebnis := (sig as *SigErgebnis)^;
var ok: int64 := 1;
if s.success != 0 then { PrintLn("FAIL: Timeout nicht erkannt"c); ok := 0; }
SigErgebnisFree(sig);
TseManagerFree(mgr);
return ok;
}
Die BSI TR-03153 (Anhang A) enthält offizielle Testvektoren für das QR-Code-Format. Beim Mock lässt sich das Format-Muster prüfen:
Format: V0;{7 Felder, semikolon-getrennt}
Feld 1: "V0" (fest)
Feld 2: TSE-Seriennummer (nicht leer)
Feld 3: Startzeitpunkt UTC (ISO 8601, endet auf "Z")
Feld 4: Endzeitpunkt UTC (ISO 8601, endet auf "Z")
Feld 5: Signaturzähler (positive ganze Zahl)
Feld 6: Anzahl Transaktionen (positive ganze Zahl)
Feld 7: Signaturwert (Base64, nicht leer)
Welche Art TSE brauche ich?
│
├── Entwicklung / Tests / CI? ──────────────────► TseMockNew()
│ (kassensichv.mock)
│
├── Stationäre Kasse, eigene Hardware gewünscht?
│ │
│ ├── USB-TSE vorhanden (Swissbit / Epson)?
│ │ └── Ja ─────────────────────────► TseFileNew(cfg)
│ │ (kassensichv.file)
│ └── Nein → weiter zu Cloud
│
└── Mobile Kasse / Online-Shop / kein Sticks-Management?
└──────────────────────────────────────────► TseRestNew(cfg)
(kassensichv.rest)
Fiskaly / Deutsche Fiskal
| Kriterium | Mock | REST (Cloud) | File (USB) |
|---|---|---|---|
| Hardware nötig | nein | nein | USB-TSE-Stick |
| Netzwerk nötig | nein | ja | nein |
| Monatliche Kosten | nein | ja (Anbietergebühr) | nein (nach Einmalkauf) |
| Geeignet für Tests | ja | Sandbox | nein (Hardware) |
| Offline-Betrieb | ja | nein | ja |
| Mobile Kasse | — | empfohlen | möglich (USB-Hub) |
| Zentrales Management | — | ja | nein |
SigErgebnis.success wird vor Bonausgabe geprüftsuccess=0 bei TseCloseBeleg) werden protokolliert und behandeltTseExportAuditData wurde getestet und ist einsatzbereitHaftungshinweis: Diese Bibliothek ist ein technisches Hilfsmittel. Kassenbetreiber sind für die gesetzeskonforme Integration und den ordnungsgemäßen Betrieb verantwortlich. Bei Unklarheiten zur rechtlichen Einordnung steuerlichen oder rechtlichen Rat einholen.