Flutter Quickstart
From zero to a certified receipt: onboard a merchant and register once, then submit sales from your app.
1. Onboard the merchant
Onboarding is a one-time server-side setup per merchant, done with your management API key (never from the app). Start with the taxpayer:
curl -X POST https://api.koolbase.com/v1/projects/$PROJECT_ID/fiscal/merchants \
-H "Authorization: Bearer $KB_KEY" \
-H "Content-Type: application/json" \
-d '{
"legal_name": "Tilla Retail Ltd",
"tax_id": "C0012345678"
}'2. Store the authority credential
Jurisdictions that certify invoices issue each taxpayer their own credential. The merchant gets theirs from the authority and you store it once — it's encrypted at rest and never readable again, not by you and not by us.
curl -X POST https://api.koolbase.com/v1/projects/$PROJECT_ID/fiscal/merchants/$MERCHANT_ID/credentials \
-H "Authorization: Bearer $KB_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "GRA production key",
"credential_type": "gh-gra-v1",
"payload": { "security_key": "..." }
}'Write-only by design
3. Activate a register
One device per till or branch. The counters start where the merchant's existing series is — if they're already at invoice 4,391, you start there. Koolbase never invents a starting point or restarts someone's series.
curl -X POST https://api.koolbase.com/v1/projects/$PROJECT_ID/fiscal/merchants/$MERCHANT_ID/devices \
-H "Authorization: Bearer $KB_KEY" \
-H "Content-Type: application/json" \
-d '{
"jurisdiction": "GH",
"adapter_id": "gh-gra",
"initial_counters": { "INV": 4391 },
"credential_id": "'$CREDENTIAL_ID'",
"authority_registration_id": "C0012345678-001"
}'
# -> {"device_id": "..."} — keep this; your app submits against itauthority_registration_id is the merchant's identity with the authority (in Ghana, their TIN-branch reference). adapter_id selects the jurisdiction: use kb-ref where no authority integration exists yet.
4. Submit a sale
From here it's your app. The payload is shaped by the device's jurisdiction — see the Ghana guide for the exact fields.
final result = await Koolbase.fiscal.submit(
deviceId: registerId,
clientRef: sale.id, // YOUR id — makes retries safe
payload: {
'kind': 'INVOICE',
'currency': 'GHS',
'exchange_rate': '1.0',
'user_name': cashier.name,
'calculation_type': 'EXCLUSIVE',
'total_amount': 4500,
'total_vat': 675,
'total_levy': 225,
'total_excise_amount': 0,
'transaction_date': '2026-01-01',
'business_partner_name': 'Cash Customer',
'business_partner_tin': 'C0000000000',
'sale_type': 'NORMAL',
'discount_type': 'GENERAL',
'discount_amount': 0,
'tax_type': 'STANDARD',
'reference': '',
'group_reference_id': '',
'purchase_order_reference': '',
'items': [
{
'item_code': 'ABT301',
'item_category': '',
'description': 'Wax print, 6 yards',
'quantity': 45,
'unit_price': 100,
'levy_a': 112.50, // NHIL 2.5%
'levy_b': 112.50, // GETFund 2.5%
'levy_d': 0, // CST
'levy_e': 0, // Tourism
'discount_amount': 0,
'excise_amount': 0,
'expire_date': '',
'batch_code': '',
},
],
},
);5. Handle the three outcomes
if (result.isFiscalized) {
printReceipt(sale, result.certification); // done in one call
} else if (result.isPending) {
await _pollThenPrint(sale, registerId); // authority still answering
} else if (result.status == FiscalStatus.blocked) {
// Nothing was consumed. Surface the reason; resubmitting the same
// clientRef after the cause is fixed resumes this exact intent.
showBlocked(result.blockedReason);
}
Future<void> _pollThenPrint(Sale sale, String registerId) async {
var intent = await Koolbase.fiscal.status(
deviceId: registerId, clientRef: sale.id);
for (var i = 0; i < 15 && intent.isPending; i++) {
await Future<void>.delayed(const Duration(seconds: 2));
intent = await Koolbase.fiscal.status(
deviceId: registerId, clientRef: sale.id);
}
if (intent.isFiscalized) printReceipt(sale, intent.certification);
}If a submit throws
status() with the same clientRef, or simply submit again. Both converge on the same single intent. FiscalException carries the server's message and status code; KoolbaseUnauthenticatedException means the API key or session was rejected.6. Render the receipt
The certification is exactly what the authority granted, in the authority's own vocabulary. In Ghana, a compliant receipt shows the invoice number, the signature, the receipt number and a scannable QR built from qr_code.
final cert = intent.certification;
if (cert == null) {
// Valid for non-certifying document kinds (a PURCHASE record is
// acknowledged, not certified). Never treat this as an error.
return;
}
final invoiceNumber = cert['num'] as String?; // CXX000000YY-001-00004391
final signature = cert['ysdcregsig'] as String?; // GRA invoice signature
final receiptNumber = cert['ysdcrecnum'] as String?;
final qrUrl = cert['qr_code'] as String?; // render as a QR imageGhana receipt requirement
qr_code URL exactly as returned — it carries the authority's verification payload.