# JavaScript SDK

The JavaScript SDK is a TypeScript-first, Node.js 20+ client for resolving receipt recipients and issuing end-to-end encrypted receipts and credit notes. Version `3.0.0` supports client-encrypted QR download envelopes, receipt recovery, per-call request options for OAuth access tokens, and idiomatic JavaScript objects with readonly service properties.

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

## Requirements

- Node.js 20 or newer
- 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

```bash
npm install @cheqi/sdk@3.0.0
```

## Initialization

With an API key:

```typescript
import { CheqiSDK, Environment } from "@cheqi/sdk";

const sdk = new CheqiSDK({
  apiEndpoint: Environment.SANDBOX,
  apiKey: process.env.CHEQI_API_KEY
});
```

With per-call OAuth access tokens only, omit `apiKey` and pass the token with [`RequestOptions`](#request-options-and-oauth):

```typescript
const sdk = new CheqiSDK({
  apiEndpoint: Environment.SANDBOX
});
```

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.TEST` | `https://test.api.cheqi.io` | `https://test.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:

```typescript
const sdk = new CheqiSDK({
  apiEndpoint: "http://localhost:8080",
  receiptDownloadBaseUrl: "http://localhost:5190",
  apiKey: process.env.CHEQI_API_KEY
});
```

Configuration also accepts `privateKey`, `timeoutSeconds`, `maxRetries`, `logger`, a Fetch-compatible `fetch` implementation, or a custom `apiClient`.
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 takes an optional `RequestOptions` object 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 `apiKey`; with neither, it fails with `AUTHENTICATION_FAILED` before any request is sent.

```typescript
await sdk.receiptService.issueReceipt(identificationDetails, receiptPayload, { accessToken });
```

The same object can also set `timeoutSeconds` 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. The request and response use the generated snake_case OAuth models:

```typescript
const token = await sdk.oauthService.requestAccessToken({
  client_id: clientId,
  client_secret: clientSecret,
  grant_id: grantId,
  scope: "receipt:issue"
});

// token.access_token, token.expires_in
```

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 plain JavaScript objects for recipient-identification data and the `createReceiptPayload` validation factory for the definitive receipt payload:

```typescript
import {
  CheqiSDK,
  Environment,
  PaymentType,
  UnitCode,
  createReceiptPayload,
  type IdentificationDetails
} from "@cheqi/sdk";

const sdk = new CheqiSDK({
  apiEndpoint: Environment.SANDBOX,
  apiKey: process.env.CHEQI_API_KEY
});

const identificationDetails = {
  paymentType: PaymentType.CARD_PAYMENT,
  cardDetails: {
    paymentAccountReference: "PAR123456789",
    cardProvider: "VISA"
  },
  recipientEmail: "customer@example.com"
} satisfies IdentificationDetails;

const receiptPayload = createReceiptPayload({
  documentNumber: "POS-2026-0001",
  issueDate: new Date(),
  currency: "EUR",
  receiptSubtotal: "10.00",
  totalBeforeTax: "10.00",
  totalTaxAmount: "2.10",
  totalAmount: "12.10",
  taxesApplied: true,
  paymentDetails: {
    paymentMeansCode: "48",
    description: "Card payment",
    cardProvider: "VISA",
    cardLastFour: "4242",
    merchantId: "MID-123",
    paymentTerminalId: "TID-456"
  },
  products: [{
    name: "Coffee beans",
    brandName: "Cheqi Coffee",
    identifier: "SKU-COFFEE-001",
    quantity: 1,
    baseQuantity: 1,
    unitCode: UnitCode.C62,
    unitPrice: "10.00",
    subtotal: "10.00",
    total: "12.10",
    taxes: [{ rate: 21, type: "VAT", taxableAmount: "10.00", amount: "2.10" }]
  }],
  taxes: [{
    rate: 21,
    type: "VAT",
    taxableAmount: "10.00",
    amount: "2.10",
    label: "VAT 21%"
  }]
});

const result = await sdk.receiptService.issueReceipt(
  identificationDetails,
  receiptPayload
);

if (result.isAccepted()) {
  console.log(result.cheqiReceiptId, result.deliveryRouteType);
} 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:

```typescript
const qrIdentification = {
  paymentType: PaymentType.CARD_PAYMENT
} satisfies IdentificationDetails;

const qrResult = await sdk.receiptService.issueReceipt(
  qrIdentification,
  receiptPayload
);

if (qrResult.downloadUrl) {
  // 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 options

`issueReceipt`, `resumeReceipt`, and `issueCreditNote` take `ReceiptIssueOptions`: the request options plus an optional `storeId`.

```typescript
// Configured API key
await sdk.receiptService.issueReceipt(identificationDetails, receiptPayload);

// Access token of the connected company
await sdk.receiptService.issueReceipt(identificationDetails, receiptPayload, { accessToken });

// Store ID and access token
await sdk.receiptService.issueReceipt(identificationDetails, receiptPayload, { storeId, accessToken });
```

## Delivery routes and fallbacks

`ReceiptResult.deliveryRouteType` 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 `CheqiSDKError` 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:

```typescript
const resumed = await sdk.receiptService.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:

```typescript
const result = await sdk.receiptService.issueDownloadReceipt(
  { paymentType: PaymentType.CASH },
  receiptPayload,
  { accessToken }
);

console.log(result.downloadUrl);
```

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:

```typescript
const completed = await sdk.receiptService.completeDownloadFallback(
  result,
  receiptEnvelope,
  templateHash,
  { 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:

```typescript
const resolution = await sdk.matchingService.matchCustomer(
  identificationDetails,
  { accessToken }
);

const delivery = sdk.encryptionService.encryptReceiptForRecipient(
  receiptPayloadJson,
  resolution.recipients[0]
);

const response = await sdk.receiptService.submitEncryptedReceipt({
  matchId: resolution.matchId,
  deviceDeliveries: [delivery]
}, { accessToken });
```

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:

```typescript
const result = await sdk.creditNoteService.issueCreditNote(
  identificationDetails,
  parentCheqiReceiptId,
  definitiveCreditNotePayload,
  { storeId, accessToken }
);
```

`storeId` is optional. The SDK serializes the supplied payload without calculations and 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:

```typescript
const request = sdk.decryptionService.decryptCreditNoteInitiationRequest(
  webhook.data.creditNoteInitiationRequest,
  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` accepts an `EncryptedReceiptDeliveryResponse` from polling or a `WebhookReceiptEnvelope` from `data.encryptedReceipt` or `data.encryptedCreditNote`:

```typescript
const envelope = sdk.decryptionService.decryptReceipt(
  webhook.data.encryptedReceipt,
  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:

```typescript
const verification = sdk.verificationService;

const cheqiHash = verification.calculateCheqiReceiptHash(cheqiReceiptJson);
const ublHash = verification.calculateUblHash(ublPurchaseReceiptXml);

const jsonMatches = verification.verifyJsonHash(cheqiReceiptJson, cheqiHash);
const xmlMatches = verification.verifyXmlHash(ublPurchaseReceiptXml, ublHash);
```

## Store management

Store operations require an OAuth access token with the relevant store permissions:

```typescript
const stores = sdk.storeService;
const options = { accessToken };

const store = await stores.createStore(companyId, createStoreRequest, options);
const allStores = await stores.getStores(companyId, undefined, options);
const activeStores = await stores.getActiveStores(companyId, options);
const selected = await stores.getStore(companyId, storeId, options);
const updated = await stores.updateStore(companyId, storeId, updateStoreRequest, options);

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

## Errors

High-level methods throw `CheqiSDKError`. API failures retain the error code, HTTP status, and correlation ID where available:

```typescript
import { CheqiSDKError } from "@cheqi/sdk";

try {
  await sdk.receiptService.issueReceipt(
    identificationDetails,
    receiptPayload,
    { accessToken }
  );
} catch (error) {
  if (error instanceof CheqiSDKError) {
    console.error(error.message, error.errorCode, error.httpStatusCode);
    if (error.hasCorrelationId()) {
      console.error("Correlation ID:", error.correlationId);
    }
  }
}
```

## Generated models

The root package exports typed model interfaces, validation factories, and enums. Every OpenAPI-generated model, enum, and serializer is also available under `Generated`:

```typescript
import { Generated } from "@cheqi/sdk";

const route = Generated.RecipientResolutionResponseDeliveryRouteTypeEnum.DIGITAL;
```

## Available services

`CheqiSDK` exposes services and configuration as readonly properties:

- `receiptService`
- `creditNoteService`
- `matchingService`
- `encryptionService`
- `decryptionService`
- `downloadService`
- `verificationService`
- `storeService`
- `companyService`
- `webhookService`
- `oauthService`
- `apiClient`
- `config`