Cheqi handles your entire receipt delivery process - from customer matching to final delivery. You send us the receipt data, and we take care of everything else:
- 🔐 End-to-end encrypted delivery to customer devices
- 📧 Automatic PDF email fallback for non-users
- 📱 Multi-device sync across all customer devices
- 🎨 Professional formatting - no design work needed
- ✅ Delivery confirmation - know when receipts are delivered
- 🌍 Country compliance - receipts automatically comply with local regulations based on where they're issued
You focus on your business, we handle the receipts.
When a customer makes a payment, identify them using payment details:
IdentificationDetails customer = IdentificationDetails.builder()
.paymentType(PaymentType.CARD_PAYMENT)
.cardDetails(CardDetails.builder()
.paymentAccountReference("PAR123456789") // From payment terminal
.cardProvider(CardProvider.VISA)
.build())
.build();IdentificationDetails customer = IdentificationDetails.builder()
.paymentType(PaymentType.IBAN_PAYMENT)
.iban("NL91ABNA0417164300")
.build();IdentificationDetails customer = IdentificationDetails.builder()
.paymentType(PaymentType.CARD_PAYMENT)
.cardDetails(CardDetails.builder()
.paymentAccountReference("PAR123456789")
.cardProvider(CardProvider.VISA)
.build())
.customerEmail("customer@example.com") // Fallback if not found
.build();Payment Account Reference (PAR) is a unique, tokenized identifier provided by payment terminals. It's more privacy-friendly than card numbers.
Create a UBL-compliant receipt template from your transaction data:
ReceiptTemplateRequest receipt = ReceiptTemplateRequest.builder()
.documentNumber("INV-2024-001")
.issueDate(Instant.now())
.currency("EUR")
.totalAmount(new BigDecimal("121.00"))
.totalTaxAmount(new BigDecimal("21.00"))
// Add products
.addProduct(Product.builder()
.name("Coffee")
.quantity(2.0)
.unitCode(UnitCode.ONE)
.unitPrice("5.00")
.subtotal("10.00")
.total("12.10")
.addTax(21.0, "VAT", "2.10")
.build())
// Add tax breakdown
.addTax(Tax.builder()
.rate(21.0)
.type("VAT")
.taxableAmount("10.00")
.amount("2.10")
.label("VAT 21%")
.build())
.build();Cheqi uses hybrid encryption for maximum security and performance:
- Generate AES Key - A unique AES-256 key is generated for each receipt
- Encrypt Receipt Data - The receipt JSON is encrypted with AES-256-GCM (authenticated encryption)
- Encrypt AES Key - The AES key is encrypted with each device's RSA-2048 public key
- Send to Backend - Encrypted receipt + encrypted keys are sent to Cheqi
Why Hybrid Encryption?
- 🔐 End-to-End Security - Only customer devices can decrypt
- ⚡ Performance - AES is fast for large data
- 🔑 Forward Secrecy - Unique keys per receipt
- 📱 Multi-Device - Encrypt once, deliver to all devices
Once encrypted receipts reach Cheqi, a two-phase delivery process begins:
For Customer Devices:
- Push Notification sent to customer's devices
- Device Downloads encrypted receipt (2 parts: receipt content + customer details)
- RSA Decryption of AES keys using device's private key
- AES Decryption of both receipt content and customer details
- Merge customer details into UBL receipt structure (
accountingCustomerParty) - Display complete receipt in Cheqi app
For Webhook Recipients (Third-Party Apps):
- Webhook Event
receipt.createdsent immediately - Payload contains:
encryptedReceipt- Receipt content encrypted with webhook's public keyencryptedCustomerDetails- Customer data encrypted separatelyreceiptId- Unique receipt identifiercreatedAt- Timestamp
Privacy by Design: Receipt content and customer details are encrypted separately. The POS system never sees customer details, and Cheqi servers cannot decrypt any data. Only authorized devices and webhook recipients can decrypt.
After the customer's device processes the receipt:
- Device merges customer details into the UBL structure
- Canonicalization - Receipt JSON is canonicalized using RFC8785
- Hash Calculation - SHA-256 hash of canonical JSON
- Submit to Cheqi - Device sends final hash to backend
- Webhook Event
receipt.finalizedsent to all webhook recipients
Finalized Webhook Payload:
{
"event": "RECEIPT_FINALIZED",
"receiptId": "abc123",
"finalHash": "a3f5d8c2e1b4...",
"finalizedAt": "2026-01-26T20:05:23Z",
"deviceId": "device-uuid"
}Webhook Recipients Can Now:
- Verify their hash matches
finalHash(integrity proof)
Important: Webhook recipients can decrypt and use the receipt immediately when they receive receipt.created in Phase 1. They don't need to wait for receipt.finalized. The finalized event is only needed for integrity verification - to confirm the receipt hash matches what the customer's device calculated.
Why Two Phases? This ensures webhook recipients get data immediately while still being able to verify the receipt's integrity against the hash calculated by the customer's device.
The SDK provides a single method that handles the entire flow:
import com.cheqi.sdk.CheqiSDK;
import com.cheqi.sdk.models.*;
import com.cheqi.sdk.receipt.ReceiptResult;
// Initialize SDK
CheqiSDK sdk = CheqiSDK.builder()
.apiEndpoint(Environment.PRODUCTION)
.apiKey(System.getenv("CHEQI_API_KEY"))
.build();
// Process complete receipt (one call does everything)
ReceiptResult result = sdk.getReceiptService()
.processCompleteReceipt(
identificationDetails, // Customer identification
receiptRequest // Receipt data
);
// Check result
if (result.isSuccess()) {
System.out.println("✅ Receipt delivered to " + result.getReceiptCount() + " devices");
} else if (result.isCustomerNotFound()) {
System.out.println("⚠️ Customer not found - prompt for email");
} else {
System.out.println("❌ Failed: " + result.getMessage());
}This single method:
- ✅ Matches the customer
- ✅ Generates the receipt template
- ✅ Encrypts for all devices
- ✅ Sends to Cheqi backend
- ✅ Returns delivery status
When a customer isn't registered with Cheqi, we automatically generate a professional PDF receipt and send it via email. No additional work required from you:
✅ Automatic PDF generation - We format your receipt data into a beautiful PDF
✅ Email delivery - We handle the entire email sending process
✅ Professional design - Branded, clean, and professional-looking receipts
✅ No extra code - Just include the email address, we do the rest
You don't need to:
- ❌ Generate PDFs yourself
- ❌ Set up email servers
- ❌ Design email templates
- ❌ Handle delivery failures
IdentificationDetails customer = IdentificationDetails.builder()
.paymentType(PaymentType.CARD_PAYMENT)
.cardDetails(CardDetails.builder()
.paymentAccountReference("PAR123456789")
.cardProvider(CardProvider.VISA)
.build())
.customerEmail("customer@example.com") // Include email
.build();
ReceiptResult result = sdk.getReceiptService()
.processCompleteReceipt(customer, receiptRequest);
if (result.isDeliveredViaEmail()) {
System.out.println("📧 Receipt sent to: " + result.getEmailAddress());
}Automatic Delivery Logic:
- ✅ Customer found → Encrypted digital receipt to Cheqi app (email ignored)
- ✅ Customer not found + email provided → Professional PDF receipt via email (we handle everything)
- ❌ Customer not found + no email →
isCustomerNotFound() = true(prompt customer for email)
Complete solution: Whether your customer uses Cheqi or not, we ensure they receive their receipt. You send us the data once, and we handle all delivery scenarios.
Third-party applications (accounting software, expense management tools, etc.) can receive real-time receipt notifications through webhooks.
Step 1: OAuth Authorization
First, obtain OAuth permission from a merchant:
// Merchant authorizes your app via OAuth flow
// You receive an access token with 'read_receipts' scopeStep 2: Create Webhook Subscription
POST /webhook/subscription
Authorization: Bearer {oauth_access_token}
Content-Type: application/json
{
"name": "My Accounting System",
"events": ["RECEIPT_CREATED", "RECEIPT_FINALIZED", "CREDIT_NOTE_CREATED"],
"notificationUrl": "https://your-app.com/webhooks/cheqi"
}Available Events:
RECEIPT_CREATED- Immediate notification with encrypted receipt dataRECEIPT_FINALIZED- Sent when customer's device submits final hashCREDIT_NOTE_CREATED- When a credit note is issuedRETURN_REQUESTED- When customer requests a return
Sent immediately when a receipt is generated:
{
"event": "RECEIPT_CREATED",
"companyId": "uuid",
"receiptId": "abc123",
"createdAt": "2026-01-26T20:00:00Z",
"encryptedReceipt": {
"encryptedReceipt": "base64...",
"encryptedSymmetricKey": "base64...",
"encryptedCustomerDetails": "base64...",
"encryptedCustomerAesKey": "base64..."
}
}What to do:
- Decrypt the receipt content using your private key
- Decrypt the customer details using your private key
- Store both parts (don't merge yet - wait for finalization)
- Mark receipt as "pending verification"
Sent when the customer's device calculates the final hash:
{
"event": "RECEIPT_FINALIZED",
"receiptId": "abc123",
"finalHash": "a3f5d8c2e1b4f7a9...",
"finalizedAt": "2026-01-26T20:05:23Z",
"deviceId": "device-uuid"
}What to do:
- Retrieve your stored receipt parts
- Merge customer details into UBL structure (
accountingCustomerPartyfield) - Canonicalize the merged JSON using the SDK's
Canonicalizerand RFC8785 - Calculate SHA-256 hash
- Verify your hash matches
finalHash - Mark receipt as "verified" if hashes match
Before Merge (Receipt Content):
{
"id": "INV-001",
"issueDate": "2026-01-26",
"accountingSupplierParty": { /* merchant info */ },
"accountingCustomerParty": null, // Empty initially
"legalMonetaryTotal": { /* totals */ },
"purchaseReceiptLines": [ /* items */ ]
}Customer Details (Separate):
{
"partyName": { "name": "John Doe" },
"postalAddress": { /* address */ },
"contact": { "electronicMail": "john@example.com" }
}After Merge (Complete Receipt):
{
"id": "INV-001",
"issueDate": "2026-01-26",
"accountingSupplierParty": { /* merchant info */ },
"accountingCustomerParty": { // Merged here
"partyName": { "name": "John Doe" },
"postalAddress": { /* address */ },
"contact": { "electronicMail": "john@example.com" }
},
"legalMonetaryTotal": { /* totals */ },
"purchaseReceiptLines": [ /* items */ ]
}// 1. Decrypt both parts
String receiptJson = decryptReceipt(encryptedReceipt, yourPrivateKey);
String customerJson = decryptCustomerDetails(encryptedCustomerDetails, yourPrivateKey);
// 2. Parse JSON
JSONObject receipt = new JSONObject(receiptJson);
JSONObject customer = new JSONObject(customerJson);
// 3. Merge customer into receipt
receipt.put("accountingCustomerParty", customer);
// 4. Canonicalize using SDK's RFC8785 Canonicalizer
String canonicalJson = RFC8785Canonicalizer.canonicalize(receipt.toString());
// 5. Calculate SHA-256 hash
String calculatedHash = SHA256.hash(canonicalJson);
// 6. Verify against finalHash from webhook
if (calculatedHash.equals(finalHash)) {
System.out.println("✅ Receipt verified!");
} else {
System.out.println("❌ Hash mismatch - receipt may be tampered");
}Security: Always verify the HMAC signature on webhook payloads using your webhook secret before processing. This ensures the webhook actually came from Cheqi.
Each webhook includes an HMAC signature in the X-Cheqi-Signature header:
String signature = request.getHeader("X-Cheqi-Signature");
String payload = request.getBody();
// Verify signature
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(webhookSecret.getBytes(), "HmacSHA256");
hmac.init(key);
String calculatedSignature = Base64.getEncoder().encodeToString(
hmac.doFinal(payload.getBytes())
);
if (!signature.equals(calculatedSignature)) {
throw new SecurityException("Invalid webhook signature");
}Cheqi uses the UBL (Universal Business Language) standard for receipts:
| Field | Type | Description |
|---|---|---|
documentNumber | String | Invoice/receipt number (e.g., "INV-001") |
issueDate | Instant | When the receipt was issued |
currency | String | ISO 4217 code (e.g., "EUR", "USD") |
totalAmount | BigDecimal | Total including tax |
totalTaxAmount | BigDecimal | Total tax amount |
Each product line item includes:
Product.builder()
.name("Product Name") // Required
.quantity(1.0) // Required
.unitCode(UnitCode.ONE) // Required (see unit codes)
.unitPrice("10.00") // Required
.subtotal("10.00") // Required (before tax)
.total("12.10") // Required (after tax)
.sku("SKU-123") // Optional
.brand("Brand Name") // Optional
.addTax(21.0, "VAT", "2.10") // Tax breakdown
.build()Receipt-level tax summary:
Tax.builder()
.rate(21.0) // Tax percentage
.type("VAT") // Tax type
.taxableAmount("10.00") // Amount before tax
.amount("2.10") // Tax amount
.label("VAT 21%") // Display label
.build()Use standardized unit codes for products:
| Category | Examples |
|---|---|
| Count | ONE, EACH, PAIR, DOZEN |
| Weight | KILOGRAM, GRAM, POUND |
| Volume | LITER, MILLILITER, GALLON_US |
| Length | METER, CENTIMETER, FOOT |
| Time | HOUR, DAY, WEEK, MONTH |
| Packaging | BOX, BOTTLE, CAN, BAG |
Note: The SDK provides all standard UBL unit codes as enums.
Track receipt delivery status:
| State | Description |
|---|---|
SUCCESS | Delivered to customer's app |
EMAIL_SENT | Sent via email (customer not found) |
CUSTOMER_NOT_FOUND | No match found, no email provided |
FAILED | Processing error occurred |
Provide email as fallback to ensure receipt delivery:
// ✅ GOOD - Email fallback
IdentificationDetails.builder()
.paymentType(PaymentType.CARD_PAYMENT)
.cardDetails(...)
.customerEmail("customer@example.com")
.build();PAR is more privacy-friendly than card numbers:
// ✅ GOOD - Use PAR from terminal
.paymentAccountReference("PAR123456789")
// ❌ AVOID - Don't use card numbers
.cardNumber("4111111111111111") // Not supportedEnsure totals match:
BigDecimal subtotal = products.stream()
.map(Product::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal taxTotal = products.stream()
.map(Product::getTaxAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal total = subtotal.add(taxTotal);
assert total.equals(receiptRequest.getTotalAmount());Implement retry logic for transient failures:
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
ReceiptResult result = sdk.getReceiptService()
.processCompleteReceipt(customer, receipt);
if (result.isSuccess()) {
break;
}
} catch (CheqiSDKException e) {
if (i == maxRetries - 1) {
// Log error and notify support
logger.error("Failed to send receipt after {} retries", maxRetries, e);
}
Thread.sleep(1000 * (i + 1)); // Exponential backoff
}
}- SDK Integration - Integrate the Java SDK into your application
- .NET SDK - Integrate the .NET SDK into your application
- Authentication - API Keys and OAuth 2.0