Kreiseck β€” Software Solutions

Kasseneck Flutter API

The Austrian RKSV-compliant cash register, right inside your Flutter app.
Issue signed receipts, take card payments and print β€” in a few lines of Dart.

pub version pub points platform RKSV compliant by Kreiseck


kasseneck_api is the official Flutter client for Kasseneck β€” a fully RKSV-compliant (Austrian Registrierkassensicherheitsverordnung) point-of-sale backend by Kreiseck Software Solutions. It takes care of the signed Datenerfassungsprotokoll, card-payment terminals, receipt printing and PDF reports, so you can focus on your app.

πŸ”‘ You need an API key & a cashregister token to operate a register. Request yours at office@kreiseck.com Β· kreiseck.com

✨ Features

  • 🧾 RKSV receipts β€” standard, cancellation, zero & training; signed JWS chain + QR code
  • πŸ’Ά All Austrian VAT rates β€” incl. the new 4.9 % Grundnahrungsmittel rate (from 1 Jul 2026)
  • πŸͺ™ Exact money β€” amounts are integer cents internally (no floating-point drift)
  • πŸ’³ Card payments out of the box β€” Hobex (Cloud & on-terminal HPS), myPOS, GP Tom, SumUp β€” and any other method via CreditCardProvider.custom
  • 🎟️ Vouchers β€” value & promo, sell & redeem, with proportional VAT split
  • πŸ’› Tips β€” per register user, cash or card; staff tips run as 0 % pass-through, owner tips as revenue split across the receipt's VAT rates
  • πŸ–¨οΈ Printing β€” Bluetooth & Wi-Fi (ESC/POS) plus the myPOS built-in printer
  • πŸ“± Drop-in receipt widget for on-screen display
  • πŸ“Š Reports & invoices β€” daily / monthly PDF
  • πŸ”— Stripe payment links for remote & online payments
  • 🀝 Partner API (package:kasseneck_api/partner.dart) β€” onboard and manage businesses on behalf of a software house, including webhook signature verification. Server-side only.

🀝 Partner API (partner.dart)

For software houses that build Kasseneck into their own product: create businesses, walk them through to a running register, then sign receipts in their name.

What the endpoints do is documented in the backend reference β€” docs/api/partner.md (long form) and docs/api/partner.llms.txt (compact, for tools and language models). This README does not repeat them; what follows is how to use the client.

The partner key (pk_live_…) belongs on a server. It can create businesses and β€” with the extra scope credentials:read β€” fetch their secrets.

import 'package:kasseneck_api/partner.dart';

final partner = PartnerApi(
  partnerKey: Platform.environment['KASSENECK_PARTNER_KEY']!,
);

final neu = await partner.createPartnerCustomer(
  appId: 'app_…',
  idempotencyKey: kundennummer, // your own β€” guards against double creation
  betrieb: {/* master data, see reference */},
  // env: PartnerEnv.test β€” allowed even with a LIVE key: rehearse the whole
  // chain without fetching a second key. Never the other way round.
);

await partner.sendPartnerCustomerFonLink(neu.customerId);
// … wait for the webhook customer.fon_verified …
await partner.requestCustomerSignature(neu.customerId);
// … wait for signature.ready …
await partner.createCustomerCashregister(customerId: neu.customerId);

The order is fixed, and every step complains with its own code when an earlier one is missing. It is available as data (kPartnerAblauf), and every error code carries a next step:

try {
  await partner.activateCashregister(customerId, cashregisterId);
} on KasseneckApiError catch (fehler) {
  if (istPartnerFehler(fehler, 'signature_not_ready')) {
    // This register's own signature is not ready yet β€” wait for signature.ready.
    print(partner.fehlerRat('signature_not_ready'));
  }
}

A rehearsal is not a register

sendPartnerWebhookTest(webhookId, event: 'cashregister.live') fires exactly the event your handler is meant to deal with β€” a connectivity probe proves nothing about the real case. So that nobody mistakes a rehearsal for the real thing, it carries test: true in the envelope:

final geprueft = leseWebhookEreignis(secrets: [secret], signaturKopf: kopf, rumpf: rumpf);
if (!geprueft.ok) return antwort(400);

if (geprueft.ereignis!.test) return antwort(200);   // rehearsal: do nothing else

Without that line somebody tells their customer the register is ready.

Credentials are a third party's secrets

getCustomerCredentials returns the business's api_key and its cashregister tokens. Whoever holds them can sign receipts in its name β€” and under RKSV a receipt cannot be taken back. They therefore come not as String but wrapped so they cannot be printed by accident:

final zugang = await partner.getCustomerCredentials(customerId);

print(zugang);            // BetriebZugangsdaten(cust_1, [apiKey Β«verborgenΒ»], 1 Kassen)
'${zugang.apiKey}';       // [apiKey Β«verborgenΒ»]

speichereVerschluesselt(zugang.apiKey.reveal());  // the only way out

Store encrypted only, never log, never put in a mail or a crash report. Every fetch is recorded and visible to the business.

Verifying incoming webhooks

This is where integrations most often fail, so it ships ready-made. Four things must hold: the raw body, the time window against replay, a constant-time comparison, and every exception treated as a rejection.

final ergebnis = leseWebhookEreignis(
  secrets: [webhookSecret],
  signaturKopf: request.headers.value('X-Kasseneck-Signature'),
  rumpf: rohBytes,          // the bytes as received β€” not re-encoded JSON
);
if (!ergebnis.ok) { /* 400, ergebnis.grund */ }
// Answer 2xx within 10 s, do the work afterwards, deduplicate on ereignis.id.

🧩 Requirements

  • Flutter Β· Dart >= 3.6
  • A Kasseneck API key + cashregister token (β†’ Kreiseck)
  • An Android device/terminal for card payments & Bluetooth printing

πŸ“¦ Installation

dependencies:
  kasseneck_api: ^5.0.0
flutter pub get

πŸš€ Quick start

import 'package:kasseneck_api/kasseneck_api.dart';
import 'package:kasseneck_api/models/kasseneck_item.dart';
import 'package:kasseneck_api/enums/vat_rate.dart';
import 'package:kasseneck_api/enums/keck_payment_method.dart';

final kasseneck = KasseneckApi(
  apiKey: 'YOUR_API_KEY',
  cashregisterToken: 'YOUR_CASHREGISTER_TOKEN',
);

// A cash sale with two items β€” prices are integer cents (320 = € 3.20)
final receipt = await kasseneck.sellReceipt(
  paymentMethod: KeckPaymentMethod.cash,
  customerDetails: ['Max Mustermann'],
  items: [
    KasseneckItem(name: 'Coffee', quantity: 2, vat: VatRate.vat20,      priceCents: 320),
    KasseneckItem(name: 'Bread',  quantity: 1, vat: VatRate.vat4komma9, priceCents: 240),
    // or, if you have euro doubles: KasseneckItem.euro(..., singlePrice: 3.20)
  ],
);

print('Receipt ${receipt?.receiptId} β€” signed: ${receipt?.signatureSuccess}');

πŸ’‘ Models & enums live in their own files β€” import the ones you use (models/…, enums/…). Payment, refund, cancellation, zero & training receipts all run through the same KasseneckApi instance.

πŸ’³ Card payments

Card payments work out of the box with several terminals β€” and you're never locked in:

Method How
Hobex Cloud (recommended) HobexCloudPayments β€” pay(...) with a resolved, three-way outcome
Hobex HPS (local terminal, recommended) HpsPayments β€” pay/refund/cancel, same three-way outcome
myPOS Β· GP Tom Β· SumUp supported & rendered on the receipt
Any other terminal/method CreditCardProvider.custom β€” just pass your own card data

Whatever terminal you use, hand the result to sellReceipt(...) as cardPaymentData and it is stored and printed on the receipt.

Why HpsPayments/HobexCloudPayments instead of calling the terminal directly: a card payment has three possible outcomes, not two β€” approved, definitely declined, or unknown (the request timed out, the connection dropped, the terminal never answered). Treating "unknown" as "declined" and retrying is how a customer gets charged twice for the same purchase. Both classes fix the transaction id before the first network call and, if the first answer is lost, resolve the same id against the terminal/cloud instead of silently starting a new attempt β€” so a lost response ends in CardPaymentOutcome.unresolved (keep the id, resolve later) rather than being guessed at.

Example β€” local Hobex terminal (HPS) β†’ signed receipt
import 'package:kasseneck_api/hobex_hps.dart'; // HpsClient, HpsPayments, HpsResult, CardPaymentOutcome, HobexReceipt

final hps = HpsPayments(HpsClient(tid: '3600335')); // TID without leading zero

// The id is fixed BEFORE the request goes out β€” persist it right away so a
// lost response can still be traced back and resolved instead of retried blind.
final transactionId = HpsClient.newTransactionId();

final result = await hps.pay(amount: 12.50, transactionId: transactionId);

switch (result.outcome) {
  case CardPaymentOutcome.approved:
    break; // proceed below
  case CardPaymentOutcome.declined:
    return; // definitely no money moved β€” safe to retry
  case CardPaymentOutcome.unresolved:
    // Not settled within the resolve budget (90 s by default, configurable).
    //
    // Do NOT retry here. Measured on a real terminal (2026-08-26): passing the same
    // transactionId again starts a SECOND card flow β€” the terminal does not recognize
    // it as the same transaction. A retry is a real second charge, not a safe repeat.
    //
    // Keep `transactionId`, resolve the outcome first β€” `HpsClient.transactionStatus(...)`
    // once the terminal answers again β€” and act only on a known outcome.
    // doc/kartenzahlung.md documents what each response code actually means.
    return;
}

// Adapt the terminal result, then create the signed receipt.
final card = HobexReceipt.fromHps(result.response!);
await kasseneck.sellReceipt(
  paymentMethod: KeckPaymentMethod.creditCard,
  creditCardProvider: card.creditCardProvider, // hobexHps
  cardPaymentId: card.transactionId,
  cardPaymentData: card.toCardPaymentData(),
  items: [KasseneckItem(name: 'Lunch', quantity: 1, vat: VatRate.vat10, priceCents: 1250)],
);

Also available: hps.refund(...), hps.cancel(...) β€” same resolved outcome. Pass an HpsObserver callback to the HpsPayments constructor to log requests, failures and how an outcome was resolved.

Example β€” Hobex Cloud β†’ signed receipt
import 'package:kasseneck_api/kasseneck_api.dart'; // HobexCloudPayments, HobexCloudResult, CardPaymentOutcome

final cloud = HobexCloudPayments(kasseneck);

// Same rule as HPS: the id is fixed by the caller before the request goes out.
final transactionId = KasseneckApi.newHobexTransactionId();

final result = await cloud.pay(transactionId: transactionId, amount: 12.50);

switch (result.outcome) {
  case CardPaymentOutcome.approved:
    break; // proceed below
  case CardPaymentOutcome.declined:
    return; // definitely no money moved β€” safe to retry
  case CardPaymentOutcome.unresolved:
    // Not settled within the resolve budget. Do NOT retry blindly β€” keep
    // `transactionId` and resolve later, see the HPS example above.
    return;
}

final card = result.receipt!;
await kasseneck.sellReceipt(
  paymentMethod: KeckPaymentMethod.creditCard,
  creditCardProvider: card.creditCardProvider,
  cardPaymentId: card.transactionId,
  cardPaymentData: card.toCardPaymentData(),
  items: [KasseneckItem(name: 'Lunch', quantity: 1, vat: VatRate.vat10, priceCents: 1250)],
);

HobexCloudPayments has no cancel() β€” a Cloud refund still goes through the raw kasseneck.hobexRefund(...) (see below), unresolved just like the plain call.

Low-level access β€” raw HpsClient / kasseneck.hobexPay(...)

Both the local HpsClient (import 'package:kasseneck_api/hobex_hps.dart';) and the Cloud calls kasseneck.hobexPay(...) / hobexRefund(...) remain available directly, for full control over the request. Neither does the outcome resolution above: a raw call that never gets an answer stays unresolved forever β€” building a payment flow directly on top of it means re-solving the exact problem HpsPayments/HobexCloudPayments already solve, with a real risk of getting the "was it charged?" question wrong under exactly the conditions (timeout, dropped connection) where getting it wrong is expensive. Reach for the raw client only when you need something the resolved wrapper doesn't expose (e.g. hps.diagnosis(), hps.transactionStatus(...)).

πŸ–¨οΈ Printing

// Bluetooth (ESC/POS)
await kasseneck.initBluetoothPrinter(printerAddress: 'AA:BB:CC:DD:EE:FF');
await receipt!.printReceiptBluetooth();

// QR garbled or missing? Printers differ in which command they support:
await receipt.printReceiptBluetooth(qrMode: QrPrintMode.imageBitImage); // or .native

// Wi-Fi
await kasseneck.initWifiPrinter('192.168.0.50', KeckPaperSize.mm80);
await receipt.printReceiptWifi();

// Open the cash drawer
await KasseneckApi.openCashDrawer();

πŸ“± On-screen receipt

A ready-made widget renders the full receipt (logo, items, VAT table, QR, card details):

KeckReceiptWidget(receipt: receipt);

πŸ“Š Reports & invoices

final monthly = await kasseneck.downloadMonthlyReport(ReportMonth.now()); // Uint8List (PDF)
final daily   = await kasseneck.downloadDailyReport(DateTime.now());
final history = await kasseneck.getReceipts(start, end);

πŸ‡¦πŸ‡Ή RKSV compliance

Every receipt is chained and signed (ES256 / JWS) and exposed as the machine-readable QR payload, exactly as required by the Austrian RKSV. Signature-device outages are detected (receipt.signatureSuccess / receipt.isSigFailed) and printed on the receipt.

πŸ—‚οΈ Versioning

This package follows semantic versioning β€” see the CHANGELOG. Latest: 5.0.0 β€” resolved card-payment outcomes (approved / declined / unresolved), transaction id fixed before the first request, and hardened receipt parsing. Breaking β€” see the CHANGELOG.

πŸ’¬ Support

Kreiseck Software Solutions β€” office@kreiseck.com Β· kreiseck.com

πŸ“„ License

See LICENSE.

Libraries

enums/cashbox_status
enums/credit_card_provider
enums/keck_invoice_payment_methode
enums/keck_month
enums/keck_paper_size
enums/keck_payment_method
enums/qr_print_mode
enums/receipt_print_type
enums/receipt_type
enums/signature_status
enums/vat_rate
enums/voucher_action
enums/voucher_type
hobex_hps
hobex Payment Service (HPS) β€” typed client for the terminal's local REST API (http://127.0.0.1:8080 when the app runs on the terminal).
kasse
Die Kasse am Tresen: Einstellungen, Warenkorb, Kassieren und Belege β€” gemeinsam von Browser-Kasse und App.
kasseneck_api
models/beleg_layout
Beleg-Zeilenmodell (Zwilling von @kreiseck/kasseneck-api/receipt ReceiptLayout): das Backend liefert es bei getReceipt als layout mit, damit App, Bondrucker, Browser-Kasse und PDF dieselben Zeilen zeigen β€” Kopf/Fuß wie beim Ausstellen des Belegs, Belegart-Aufdruck (STORNOBELEG, TRAININGSBELEG, NULLBELEG …), reduzierter Nullbeleg, Testkasse/Testsignatur.
models/beleg_raster
Zeichenraster (Zwilling von renderReceiptGrid im JS-Paket @kreiseck/kasseneck-api): der Beleg als Zeilen mit exakt N Zeichen (58 mm = 32, 80 mm = 48) β€” die eine Wahrheit fΓΌr Bildschirm, Bondruck und PDF. Regeln (fest, damit ΓΌberall dasselbe herauskommt):
models/cashregister
models/hobex_receipt
models/kasseneck_item
models/kasseneck_receipt
models/keck_customer
models/keck_customer_address
models/keck_invoice
models/keck_invoice_item
models/keck_print_result
models/keck_sepa_info
models/keck_tip
models/keck_tip_person
models/keck_user
models/keck_voucher
models/report_month
models/stripe_url_seesion
models/sumup_checkout_response
partner
Partner-API: alles, was ein Partner-Softwarehaus ΓΌber die Kasseneck-Schnittstelle tut β€” Betriebe anlegen und bis zur laufenden Kasse begleiten, danach in ihrem Namen Belege signieren.
printing
Oeffentliches Barrel fuer den vendierten ESC/POS-Druck-Stack.
register
Kopplung und Anmeldung eines KassengerΓ€ts (RKSV-Kasse).
services/keck_printer
services/logo_service
services/printer_service
services/rksv_service
services/sumup_service
services/vienna_time
widgets/keck_receipt_lines_widget
widgets/keck_receipt_widget