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.
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 sameKasseneckApiinstance.
π³ 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/stripe_link_mode
- 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:8080when 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/receiptReceiptLayout): das Backend liefert es beigetReceiptalslayoutmit, 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
renderReceiptGridim 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/print_paper
- 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