# Java SDK

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, 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](https://github.com/cheqi-io/cheqi-sdk-java)

## Requirements

- 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


## Installation

The implementation documented here is version `3.0.0`.

Maven:

```xml
<dependency>
    <groupId>io.cheqi</groupId>
    <artifactId>cheqi-sdk</artifactId>
    <version>3.0.0</version>
</dependency>
```

Gradle:

```gradle
implementation 'io.cheqi:cheqi-sdk:3.0.0'
```

If `3.0.0` is not yet available in your configured artifact repository, build the source branch and install it locally:

```bash
./gradlew publishToMavenLocal
```

## Initialization

With an API key:

```java
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 only, omit `.apiKey(...)` and pass the token with [`RequestOptions`](#request-options-and-oauth):

```java
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:

```java
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.

## Request options and OAuth

Every API method has an overload that takes `RequestOptions` as its last argument. Use it to act on behalf of a connected company with its OAuth access token, or to override the timeout or retry count for one call. Without an access token, the call uses the configured API key; with neither, it fails with `AUTHENTICATION_FAILED` before any request is sent.

```java
import com.cheqi.sdk.http.RequestOptions;

RequestOptions options = RequestOptions.withAccessToken(accessToken);

ReceiptResult result = sdk.getReceiptService()
    .issueReceipt(identificationDetails, receiptPayload, storeId, options);
```

`RequestOptions.builder()` can also set `timeout` and `maxRetries` for a single call. `maxRetries` applies only to operations the SDK retries; encrypted submissions are never retried automatically.

Company access tokens are short-lived and bound to one Grant, so an integration that serves several companies holds one token per Grant. Request one with the client-credentials grant:

```java
import com.cheqi.sdk.models.generated.ClientCredentialsTokenRequest;
import com.cheqi.sdk.models.generated.OAuthTokenExchangeResponse;

OAuthTokenExchangeResponse token = sdk.getOAuthService().requestAccessToken(
    new ClientCredentialsTokenRequest()
        .clientId(clientId)
        .clientSecret(clientSecret)
        .grantId(grantId)
        .scope("receipt:issue"));

// token.getAccessToken(), token.getExpiresIn()
```

The SDK does not cache or refresh tokens. Store each token with its expiry in your own token store, keyed by Grant and scope, request a new one before it expires, and pass the current token with `RequestOptions`. See [OAuth 2.0](/authentication/oauth).

## Issue a receipt

Use the convenience builders in `com.cheqi.sdk.models` for the definitive receipt body and the generated models for identification and payment details:

```java
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))
    .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.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.

### Card payment with only the last four digits

When the terminal has no PAR or other reliable customer identifier, do not
place the four digits in `IdentificationDetails.cardDetails` as a substitute
for matching. Keep `paymentType`, and include the digits in the definitive
encrypted payload's `paymentDetails.cardLastFour` as shown above:

```java
IdentificationDetails qrIdentification = new IdentificationDetails()
    .paymentType(PaymentType.CARD_PAYMENT);

ReceiptResult qrResult = sdk.getReceiptService()
    .issueReceipt(qrIdentification, receiptPayload);

if (qrResult.getDownloadUrl() != null) {
    // Render the full URL as a QR code or customer-facing link.
}
```

Enable download fallback for this route. The SDK can return one client-encrypted
URL without a customer match; show it as a QR at a terminal or as a link and QR
on a webshop page. See [Web Wallet](/receipts/qr-collection).

### Authentication and store overloads

The `issueReceipt` overloads combine an optional `UUID` store ID with optional `RequestOptions`:

```java
// Configured API key
ReceiptResult apiKeyResult = sdk.getReceiptService()
    .issueReceipt(identificationDetails, receiptPayload);

// Access token of the connected company
ReceiptResult oauthResult = sdk.getReceiptService()
    .issueReceipt(identificationDetails, receiptPayload, RequestOptions.withAccessToken(accessToken));

// Store ID with configured API key
ReceiptResult storeResult = sdk.getReceiptService()
    .issueReceipt(identificationDetails, receiptPayload, storeId);

// Store ID and access token
ReceiptResult storeOAuthResult = sdk.getReceiptService()
    .issueReceipt(identificationDetails, receiptPayload, storeId, RequestOptions.withAccessToken(accessToken));
```

## Delivery routes and fallbacks

`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 when `IdentificationDetails.paymentType` is available. Otherwise, it returns `isDownloadEnvelopeRequired()`.


An email address in `IdentificationDetails` can identify an existing Cheqi
user for `DIGITAL` delivery. Otherwise, the enabled download fallback gives the
customer a receipt link they can open in the browser wallet.

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.

### Recover an uncertain digital submission

If `issueReceipt` returns `DIGITAL_PENDING`, retain its `matchId` and the same
definitive `ReceiptPayload`. Resume that match rather than resolving the
customer and issuing a second receipt:

```java
ReceiptResult resumed = sdk.getReceiptService()
    .resumeReceipt(matchId, receiptPayload);
```

The SDK checks the original match before submitting again. It returns the
original acceptance result if the receipt was already accepted, or a pending
result with `retryAfterSeconds` while processing continues. An expired,
unsubmitted match cannot be resumed. See [Sending Receipts](/receipts/sending#submission-rules).

### Explicit download receipt

For a known customer-without-Cheqi flow, skip matching:

```java
IdentificationDetails cashCustomer = new IdentificationDetails()
    .paymentType(PaymentType.CASH);

ReceiptResult result = sdk.getReceiptService().issueDownloadReceipt(
    cashCustomer,
    receiptPayload,
    RequestOptions.withAccessToken(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:

```java
ReceiptResult completed = sdk.getReceiptService().completeDownloadFallback(
    result,
    receiptEnvelope,
    templateHash,
    RequestOptions.withAccessToken(accessToken)
);
```

Your integration owns durable storage, scheduling, retry, monitoring, and retention for deferred download work. See [Download links](/receipts/download-fallback) for the encryption and recovery contract.

## Matching and lower-level encryption

Use the service APIs when you need to control matching, encryption, or submission separately:

```java
RecipientResolutionResponse resolution = sdk.getMatchingService()
    .matchCustomer(identificationDetails, options);

EncryptedReceiptPayload delivery = sdk.getEncryptionService()
    .encryptReceiptForRecipient(
        receiptPayloadJson,
        resolution.getRecipients().get(0)
    );

ReceiptSubmissionResponse response = sdk.getReceiptService()
    .submitEncryptedReceipt(encryptedReceiptEnvelope, options);
```

The caller must honor the selected route, encrypt for every owner device, and preserve identical definitive plaintext across device encryptions.

## Credit notes

Merchant-issued credit notes use the same recipient-resolution and per-device encryption model:

```java
CreditNoteResult result = sdk.getCreditNoteService().issueCreditNote(
    identificationDetails,
    parentCheqiReceiptId,
    definitiveCreditNotePayload,
    RequestOptions.withAccessToken(accessToken)
);
```

The five-argument overload accepts a `UUID storeId` before the `RequestOptions`. 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:

```java
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](/creditnote/receiving-returns) and [Issuing Credit Notes](/creditnote/issuing-credit-notes).

## Receipt and webhook decryption

`decryptReceipt` is overloaded for an `EncryptedReceiptDeliveryResponse` from polling and a `WebhookReceiptEnvelope` from `data.encryptedReceipt` or `data.encryptedCreditNote`:

```java
ReceiptEnvelope envelope = sdk.getDecryptionService()
    .decryptReceipt(encryptedReceiptDelivery, privateKeyBase64);
```

The plaintext is already a complete `ReceiptEnvelope`; no backend-context merge step is required.

## Verification

The verification service implements RFC 8785 canonical JSON hashing and Exclusive XML Canonicalization 1.0 hashing:

```java
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 management

Store operations use `UUID` identifiers and require an OAuth access token with the relevant store permissions:

```java
StoreService stores = sdk.getStoreService();
RequestOptions options = RequestOptions.withAccessToken(accessToken);

StoreDTO store = stores.createStore(companyId, createStoreRequest, options);
List<StoreDTO> allStores = stores.getStores(companyId, options);
List<StoreDTO> activeStores = stores.getActiveStores(companyId, options);
StoreDTO selected = stores.getStore(companyId, storeId, options);
StoreDTO updated = stores.updateStore(companyId, storeId, createStoreRequest, options);

stores.activateStore(companyId, storeId, options);
stores.deactivateStore(companyId, storeId, options);
stores.deleteStore(companyId, storeId, options);
```

The current `updateStore` implementation accepts `CreateStoreRequest` as its request type.

## Errors

High-level receipt and credit-note methods throw `CheqiSDKException`. Direct HTTP client methods throw `CheqiApiException`:

```java
import com.cheqi.sdk.exceptions.CheqiSDKException;

try {
    sdk.getReceiptService()
        .issueReceipt(identificationDetails, receiptPayload, options);
} 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());
    }
}
```

## Available services

`CheqiSDK` exposes the implemented services and lower-level API client through:

- `getReceiptService()`
- `getCreditNoteService()`
- `getMatchingService()`
- `getEncryptionService()`
- `getDecryptionService()`
- `getDownloadService()`
- `getVerificationService()`
- `getStoreService()`
- `getCompanyService()`
- `getWebhookService()`
- `getOAuthService()`
- `getApiClient()`
- `getConfig()`