Receipt webhooks deliver encrypted, recipient-specific document bundles and their definitive CHEQI hash to authorized integrations.
{
"name": "Receipt integration",
"notificationUrl": "https://your-domain.example/webhooks/cheqi",
"events": ["RECEIPT_CREATED"]
}See Webhook Setup for registration and Webhook Security for signature verification.
- A merchant submits receipt-generation input encrypted independently for the matched owner devices.
- Each owner device decrypts its job and generates its local CHEQI receipt with the shared Rust engine.
- One leased device resolves current downstream recipients.
- That device builds a
ReceiptEnvelopefor each recipient and encrypts it using that recipient's key. - Cheqi stores and routes the ciphertext without decrypting it.
- Cheqi sends
RECEIPT_CREATEDto the subscribed receiving integration.
{
"event": "RECEIPT_CREATED",
"data": {
"encryptedReceipt": {
"clientId": "your-public-client-id",
"companyId": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2026-08-03T13:30:00Z",
"cheqiReceiptId": "CHQ-20260803-ABC123",
"encryptedEnvelope": "base64-ciphertext...",
"encryptedEnvelopeKey": "base64-wrapped-aes-key...",
"publicKey": "base64-recipient-public-key-snapshot...",
"recipientKeyAlgorithm": "RSA_2048",
"envelopeVersion": 1,
"receiptGeneratorVersion": "0.3.0",
"finalHash": "sha256-cheqi-document-hash..."
}
}
}finalHash is the definitive CHEQI document hash submitted by the elected device together with the encrypted deliveries. A separate finalization event is not required.
The ciphertext and wrapped key are the values originally submitted by the elected owner device. Cheqi does not decrypt, rebuild, or merge the receipt documents.
After unwrapping the AES key and decrypting encryptedEnvelope, deserialize the plaintext JSON into ReceiptEnvelope:
public record ReceiptEnvelope(
int envelopeVersion,
UUID receiptUuid,
String cheqiReceiptId,
String receiptGeneratorVersion,
Map<String, ReceiptEnvelopeDocument> documents
) {}
public record ReceiptEnvelopeDocument(
String mediaType,
String content
) {}Example processing logic:
WebhookPayload receipt = event.data().encryptedReceipt();
byte[] plaintext = envelopeCrypto.decrypt(
receipt.encryptedEnvelope(),
receipt.encryptedEnvelopeKey(),
receipt.publicKey(),
receipt.recipientKeyAlgorithm()
);
ReceiptEnvelope envelope = objectMapper.readValue(
plaintext,
ReceiptEnvelope.class
);
ReceiptEnvelopeDocument cheqi = envelope.documents().get("CHEQI");
ReceiptEnvelopeDocument invoice = envelope.documents().get("UBL_INVOICE");Use the SDK's envelope crypto implementation rather than inventing a separate wire format. The public-key snapshot lets an integration with rotated keys select the historical private key used for this delivery.
Inspect ReceiptEnvelope.documents to determine what was delivered. The webhook does not include receiptFormats, encryptedCustomerDetails, or encryptedCustomerAesKey.
Verify the signature using the exact raw body before parsing it:
public ResponseEntity<Void> receive(String rawBody, String signature) {
if (!signatureVerifier.isValid(rawBody, signature)) {
return ResponseEntity.status(401).build();
}
WebhookEvent event = objectMapper.readValue(rawBody, WebhookEvent.class);
webhookQueue.enqueue(event);
return ResponseEntity.ok().build();
}In the asynchronous worker, route RECEIPT_CREATED to the envelope-decryption and reconciliation path. Store events idempotently because delivery can be retried.
For the complete event schemas, see Webhook Events.