The Java SDK is a Java 11+ client for resolving receipt recipients and issuing end-to-end encrypted receipts and credit notes. It also provides client-encrypted receipt downloads, receipt-envelope decryption, integrity helpers, and store management.
The SDK preserves Cheqi's zero-knowledge boundary: your integration supplies the definitive receipt values, the SDK encrypts them locally for every matched owner device, and Cheqi receives ciphertext rather than the plaintext receipt body. The SDK does not calculate, enrich, or reconcile receipt values.
Source code and releases: cheqi-io/cheqi-sdk-java
- Java 11 or newer
- Maven 3.6 or newer, or Gradle
- an API key, or an OAuth access token with the required permissions
- a base64-encoded PKCS#8 RSA private key when using recipient-side decryption
The implementation documented here is version 2.1.0.
Maven:
<dependency>
<groupId>io.cheqi</groupId>
<artifactId>cheqi-sdk</artifactId>
<version>2.1.0</version>
</dependency>Gradle:
implementation 'io.cheqi:cheqi-sdk:2.1.0'If 2.1.0 is not yet available in your configured artifact repository, build the source branch and install it locally:
./gradlew publishToMavenLocalWith an API key:
import com.cheqi.sdk.CheqiSDK;
import com.cheqi.sdk.config.Environment;
CheqiSDK sdk = CheqiSDK.builder()
.apiEndpoint(Environment.SANDBOX)
.apiKey(System.getenv("CHEQI_API_KEY"))
.build();With per-call OAuth access tokens, omit .apiKey(...) and pass the token to the service method:
CheqiSDK sdk = CheqiSDK.builder()
.apiEndpoint(Environment.SANDBOX)
.build();Standard environments configure both the API endpoint and customer-facing receipt origin:
| Environment | API endpoint | Receipt origin |
|---|---|---|
Environment.SANDBOX | https://sandbox.api.cheqi.io | https://sandbox.receipt.cheqi.io |
Environment.PRODUCTION | https://api.cheqi.io | https://receipt.cheqi.io |
For a custom deployment, configure both URLs if you use download receipts:
CheqiSDK sdk = CheqiSDK.builder()
.customApiEndpoint("http://localhost:8080")
.receiptDownloadBaseUrl("http://localhost:5190")
.apiKey(System.getenv("CHEQI_API_KEY"))
.build();The builder also exposes .privateKey(...), .timeoutSeconds(...), .maxRetries(...), and .httpClient(...). The current decryption methods still take privateKeyBase64 explicitly, even when a private key is present in the SDK configuration.
Use the convenience builders in com.cheqi.sdk.models for the definitive receipt body and the generated models for identification and payment details:
import com.cheqi.sdk.CheqiSDK;
import com.cheqi.sdk.config.Environment;
import com.cheqi.sdk.models.Product;
import com.cheqi.sdk.models.ReceiptPayload;
import com.cheqi.sdk.models.Tax;
import com.cheqi.sdk.models.generated.CardDetails;
import com.cheqi.sdk.models.generated.IdentificationDetails;
import com.cheqi.sdk.models.generated.PaymentDetails;
import com.cheqi.sdk.models.generated.PaymentType;
import com.cheqi.sdk.models.generated.UnitCode;
import com.cheqi.sdk.receipt.ReceiptResult;
import java.time.OffsetDateTime;
CheqiSDK sdk = CheqiSDK.builder()
.apiEndpoint(Environment.SANDBOX)
.apiKey(System.getenv("CHEQI_API_KEY"))
.build();
IdentificationDetails identificationDetails = new IdentificationDetails()
.paymentType(PaymentType.CARD_PAYMENT)
.cardDetails(new CardDetails()
.paymentAccountReference("PAR123456789")
.cardProvider(CardDetails.CardProviderEnum.VISA)
.lastFourDigits("4242"))
.recipientEmail("customer@example.com");
ReceiptPayload receiptPayload = ReceiptPayload.builder()
.documentNumber("POS-2026-0001")
.issueDate(OffsetDateTime.now())
.currency("EUR")
.receiptSubtotal("10.00")
.totalBeforeTax("10.00")
.totalTaxAmount("2.10")
.totalAmount("12.10")
.taxesApplied(true)
.paymentDetails(new PaymentDetails()
.paymentMeansCode("48")
.description("Card payment")
.cardProvider("VISA")
.cardLastFour("4242")
.merchantId("MID-123")
.paymentTerminalId("TID-456"))
.addProduct(Product.builder()
.name("Coffee beans")
.brandName("Cheqi Coffee")
.identifier("SKU-COFFEE-001")
.quantity(1.0)
.baseQuantity(1.0)
.unitCode(UnitCode.C62)
.unitPrice("10.00")
.subtotal("10.00")
.total("12.10")
.addTax(21.0, "VAT", "10.00", "2.10")
.build())
.addTax(Tax.builder()
.rate(21.0)
.type("VAT")
.taxableAmount("10.00")
.amount("2.10")
.label("VAT 21%")
.build())
.build();
ReceiptResult result = sdk.getReceiptService()
.issueReceipt(identificationDetails, receiptPayload);
if (result.isAccepted()) {
System.out.println("Receipt accepted: " + result.getCheqiReceiptId());
System.out.println("Delivery route: " + result.getDeliveryRouteType());
} else if (result.isEmailReceiptRequired()) {
// Generate and submit the permitted email-fallback receipt explicitly.
} else if (result.isDownloadEnvelopeRequired()) {
// Generate the final ReceiptEnvelope locally, then complete the fallback.
}ReceiptPayload is definitive. Supply taxesApplied, totals, line values, payment presentation, and jurisdictional data yourself. IdentificationDetails is only matching and local fallback context; the SDK does not copy it into ReceiptPayload.paymentDetails on the digital route.
The implemented issueReceipt overloads support API-key and OAuth calls, with an optional UUID store ID:
// Configured API key
ReceiptResult apiKeyResult = sdk.getReceiptService()
.issueReceipt(identificationDetails, receiptPayload);
// Per-call OAuth token
ReceiptResult oauthResult = sdk.getReceiptService()
.issueReceipt(identificationDetails, receiptPayload, accessToken);
// Store ID with configured API key
ReceiptResult storeResult = sdk.getReceiptService()
.issueReceipt(identificationDetails, receiptPayload, storeId);
// Store ID and per-call OAuth token
ReceiptResult storeOAuthResult = sdk.getReceiptService()
.issueReceipt(identificationDetails, receiptPayload, storeId, accessToken);ReceiptResult.getDeliveryRouteType() is the authoritative route:
DIGITAL: the SDK encrypts the exact serialized payload independently for every matched owner device and submits it immediately.DOWNLOAD_FALLBACK: the SDK creates and uploads a client-encrypted download whenIdentificationDetails.paymentTypeis available. Otherwise, it returnsisDownloadEnvelopeRequired().EMAIL_FALLBACK: the SDK returnsisEmailReceiptRequired().issueReceiptdoes not send the email automatically.
If recipient resolution returns routeFound: false, issueReceipt throws CheqiSDKException with error code CUSTOMER_NOT_FOUND; it does not return a synthetic customer-not-found result.
For a known customer-without-Cheqi flow, skip matching:
IdentificationDetails cashCustomer = new IdentificationDetails()
.paymentType(PaymentType.CASH);
ReceiptResult result = sdk.getReceiptService().issueDownloadReceipt(
cashCustomer,
receiptPayload,
accessToken
);
String downloadUrl = result.getDownloadUrl();The content key is generated locally and remains in the URL fragment; Cheqi receives only the download ID, ciphertext, and template hash. Anyone with the complete URL can decrypt the receipt, so deliver it through an appropriate customer-facing channel.
If the matched fallback needs a caller-generated final envelope, complete it with:
ReceiptResult completed = sdk.getReceiptService().completeDownloadFallback(
result,
receiptEnvelope,
templateHash,
accessToken
);Your integration owns durable storage, scheduling, retry, monitoring, and retention for deferred download work. See Download links for the encryption and recovery contract.
Use the service APIs when you need to control matching, encryption, or submission separately:
RecipientResolutionResponse resolution = sdk.getMatchingService()
.matchCustomer(identificationDetails, accessToken);
EncryptedReceiptPayload delivery = sdk.getEncryptionService()
.encryptReceiptForRecipient(
receiptPayloadJson,
resolution.getRecipients().get(0)
);
ReceiptSubmissionResponse response = sdk.getReceiptService()
.submitEncryptedReceipt(encryptedReceiptEnvelope, accessToken);The caller must honor the selected route, encrypt for every owner device, and preserve identical definitive plaintext across device encryptions.
Merchant-issued credit notes use the same recipient-resolution and per-device encryption model:
CreditNoteResult result = sdk.getCreditNoteService().issueCreditNote(
identificationDetails,
parentCheqiReceiptId,
definitiveCreditNotePayload,
accessToken
);The five-argument overload accepts a UUID storeId before accessToken. The SDK serializes the supplied Object or uses a supplied JSON String unchanged, then submits it through the separate encrypted credit-note endpoint.
Customer return requests are separate from merchant-issued credit notes. Decrypt, deserialize, and validate a polling or webhook request in one call:
CreditNoteInitiationRequest request = sdk.getDecryptionService()
.decryptCreditNoteInitiationRequest(
webhook.getData().getCreditNoteInitiationRequest(),
privateKeyBase64
);The validation covers identifiers, positive quantities, reason codes, note lengths, refund preference, and bank-account requirements. Your integration remains responsible for eligibility, remaining quantities, accepted outcomes, taxes, and the definitive refund. See Receiving Return Requests and Issuing Credit Notes.
decryptReceipt is overloaded for an EncryptedReceiptDeliveryResponse from polling and a WebhookReceiptEnvelope from data.encryptedReceipt or data.encryptedCreditNote:
ReceiptEnvelope envelope = sdk.getDecryptionService()
.decryptReceipt(encryptedReceiptDelivery, privateKeyBase64);The plaintext is already a complete ReceiptEnvelope; no backend-context merge step is required.
The verification service implements RFC 8785 canonical JSON hashing and Exclusive XML Canonicalization 1.0 hashing:
VerificationService verification = sdk.getVerificationService();
String cheqiHash = verification.calculateCheqiReceiptHash(cheqiReceiptJson);
String ublHash = verification.calculateUblHash(ublPurchaseReceiptXml);
String canonicalJson = verification.canonicalizeCheqiReceipt(cheqiReceiptJson);
String canonicalXml = verification.canonicalizeUbl(ublPurchaseReceiptXml);Store operations use UUID identifiers and require an OAuth access token with the relevant store permissions:
StoreService stores = sdk.getStoreService();
StoreDTO store = stores.createStore(companyId, createStoreRequest, accessToken);
List<StoreDTO> allStores = stores.getStores(companyId, accessToken);
List<StoreDTO> activeStores = stores.getActiveStores(companyId, accessToken);
StoreDTO selected = stores.getStore(companyId, storeId, accessToken);
StoreDTO updated = stores.updateStore(companyId, storeId, createStoreRequest, accessToken);
stores.activateStore(companyId, storeId, accessToken);
stores.deactivateStore(companyId, storeId, accessToken);
stores.deleteStore(companyId, storeId, accessToken);The current updateStore implementation accepts CreateStoreRequest as its request type.
High-level receipt and credit-note methods throw CheqiSDKException. Direct HTTP client methods throw CheqiApiException:
import com.cheqi.sdk.exceptions.CheqiSDKException;
try {
sdk.getReceiptService()
.issueReceipt(identificationDetails, receiptPayload, accessToken);
} catch (CheqiSDKException exception) {
System.err.println(exception.getMessage());
System.err.println("Error code: " + exception.getErrorCode());
if (exception.hasCorrelationId()) {
System.err.println("Correlation ID: " + exception.getCorrelationId());
}
}CheqiSDK exposes the implemented services and lower-level API client through:
getReceiptService()getCreditNoteService()getMatchingService()getEncryptionService()getDecryptionService()getDownloadService()getVerificationService()getStoreService()getApiClient()getConfig()