Skip to content
Last updated

How It Works

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.

Customer's DeviceWebhook RecipientCheqi APIYour POS SystemCustomer's DeviceWebhook RecipientCheqi APIYour POS System1. Match customer (card/IBAN)2. Return device public keys + webhook keys3. Generate receipt template4. Return UBL receipt JSON + template hash5. Encrypt receipt for devices & webhooks6. Send encrypted receipts7a. Webhook: receipt.created (encrypted data)7b. Push notification to devices8. Decrypt receipt data9. Merge customer details into UBL10. Calculate final hash (RFC8785 + SHA-256)11. Submit final hash12. Webhook: receipt.finalized (final hash)13. Verify: decrypt → merge → hash → compare

The Complete Flow

1. Customer Identification

When a customer makes a payment, identify them using payment details:

Card Payment

    IdentificationDetails customer = IdentificationDetails.builder()
        .paymentType(PaymentType.CARD_PAYMENT)
        .cardDetails(CardDetails.builder()
            .paymentAccountReference("PAR123456789")  // From payment terminal
            .cardProvider(CardProvider.VISA)
            .build())
        .build();

IBAN Payment

IdentificationDetails customer = IdentificationDetails.builder()
    .paymentType(PaymentType.IBAN_PAYMENT)
    .iban("NL91ABNA0417164300")
    .build();

Email Fallback

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)

Payment Account Reference (PAR) is a unique, tokenized identifier provided by payment terminals. It's more privacy-friendly than card numbers.

2. Receipt Template Generation

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();

3. Hybrid Encryption

Cheqi uses hybrid encryption for maximum security and performance:

  1. Generate AES Key - A unique AES-256 key is generated for each receipt
  2. Encrypt Receipt Data - The receipt JSON is encrypted with AES-256-GCM (authenticated encryption)
  3. Encrypt AES Key - The AES key is encrypted with each device's RSA-2048 public key
  4. 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

4. Delivery & Decryption

Once encrypted receipts reach Cheqi, a two-phase delivery process begins:

Phase 1: Immediate Delivery (receipt.created)

For Customer Devices:

  1. Push Notification sent to customer's devices
  2. Device Downloads encrypted receipt (2 parts: receipt content + customer details)
  3. RSA Decryption of AES keys using device's private key
  4. AES Decryption of both receipt content and customer details
  5. Merge customer details into UBL receipt structure (accountingCustomerParty)
  6. Display complete receipt in Cheqi app

For Webhook Recipients (Third-Party Apps):

  1. Webhook Event receipt.created sent immediately
  2. Payload contains:
    • encryptedReceipt - Receipt content encrypted with webhook's public key
    • encryptedCustomerDetails - Customer data encrypted separately
    • receiptId - Unique receipt identifier
    • createdAt - Timestamp
Privacy by Design

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.

Phase 2: Finalization (receipt.finalized)

After the customer's device processes the receipt:

  1. Device merges customer details into the UBL structure
  2. Canonicalization - Receipt JSON is canonicalized using RFC8785
  3. Hash Calculation - SHA-256 hash of canonical JSON
  4. Submit to Cheqi - Device sends final hash to backend
  5. Webhook Event receipt.finalized sent 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

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?

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.

One-Method Integration

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

Email Fallback - We Handle Everything

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 emailisCustomerNotFound() = true (prompt customer for email)
Complete Solution

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.

Webhook Integration for Third-Party Apps

Third-party applications (accounting software, expense management tools, etc.) can receive real-time receipt notifications through webhooks.

Subscribing to 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' scope

Step 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 data
  • RECEIPT_FINALIZED - Sent when customer's device submits final hash
  • CREDIT_NOTE_CREATED - When a credit note is issued
  • RETURN_REQUESTED - When customer requests a return

Receiving Webhooks

Event 1: receipt.created

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:

  1. Decrypt the receipt content using your private key
  2. Decrypt the customer details using your private key
  3. Store both parts (don't merge yet - wait for finalization)
  4. Mark receipt as "pending verification"

Event 2: receipt.finalized

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:

  1. Retrieve your stored receipt parts
  2. Merge customer details into UBL structure (accountingCustomerParty field)
  3. Canonicalize the merged JSON using the SDK's Canonicalizer and RFC8785
  4. Calculate SHA-256 hash
  5. Verify your hash matches finalHash
  6. Mark receipt as "verified" if hashes match

Receipt Structure & Merging

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 */ ]
}

Hash Verification Example

// 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

Security: Always verify the HMAC signature on webhook payloads using your webhook secret before processing. This ensures the webhook actually came from Cheqi.

Webhook Security

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");
}

Receipt Data Model

Cheqi uses the UBL (Universal Business Language) standard for receipts:

Required Fields

FieldTypeDescription
documentNumberStringInvoice/receipt number (e.g., "INV-001")
issueDateInstantWhen the receipt was issued
currencyStringISO 4217 code (e.g., "EUR", "USD")
totalAmountBigDecimalTotal including tax
totalTaxAmountBigDecimalTotal tax amount

Products

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()

Tax Breakdown

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()

Unit Codes

Use standardized unit codes for products:

CategoryExamples
CountONE, EACH, PAIR, DOZEN
WeightKILOGRAM, GRAM, POUND
VolumeLITER, MILLILITER, GALLON_US
LengthMETER, CENTIMETER, FOOT
TimeHOUR, DAY, WEEK, MONTH
PackagingBOX, BOTTLE, CAN, BAG

Note: The SDK provides all standard UBL unit codes as enums.

Receipt States

Track receipt delivery status:

StateDescription
SUCCESSDelivered to customer's app
EMAIL_SENTSent via email (customer not found)
CUSTOMER_NOT_FOUNDNo match found, no email provided
FAILEDProcessing error occurred

Best Practices

Always Include Email When Available

Provide email as fallback to ensure receipt delivery:

// ✅ GOOD - Email fallback
IdentificationDetails.builder()
    .paymentType(PaymentType.CARD_PAYMENT)
    .cardDetails(...)
    .customerEmail("customer@example.com")
    .build();

Use Payment Account Reference (PAR)

PAR is more privacy-friendly than card numbers:

// ✅ GOOD - Use PAR from terminal
.paymentAccountReference("PAR123456789")

// ❌ AVOID - Don't use card numbers
.cardNumber("4111111111111111")  // Not supported

Validate Amounts Before Sending

Ensure 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());

Handle Errors Gracefully

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
    }
}

Next Steps