# .NET SDK

The .NET SDK is a .NET 8+ 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. Its APIs are asynchronous (`Task`), accept a `CancellationToken`, and integrate with dependency injection and `Microsoft.Extensions.Logging`.

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-dotnet](https://github.com/cheqi-io/cheqi-sdk-dotnet) · Package: [cheqi-sdk on NuGet](https://www.nuget.org/packages/cheqi-sdk)

## Requirements

- .NET 8 or newer
- an API key, or an OAuth access token with the required permissions
- a PKCS#8 RSA private key (PEM or base64 DER) when using recipient-side decryption


## Installation

The implementation documented here is version `3.0.0`.

```bash
dotnet add package cheqi-sdk --version 3.0.0
```

## Initialization

With an API key:

```csharp
using Cheqi.Sdk;
using Cheqi.Sdk.Config;

using var sdk = CheqiSdk.Create(new CheqiSdkOptions
{
    Environment = CheqiApiEnvironment.Sandbox,
    ApiKey = System.Environment.GetEnvironmentVariable("CHEQI_API_KEY")
});
```

Reuse one SDK instance for the lifetime of your application. With per-call OAuth access tokens only, omit `ApiKey` and pass the token with [`RequestOptions`](#request-options-and-oauth).

Register the SDK with dependency injection:

```csharp
using Cheqi.Sdk.Extensions;

services.AddCheqiSdk(options =>
{
    options.Environment = CheqiApiEnvironment.Production;
    options.ApiKey = configuration["Cheqi:ApiKey"];
});
```

Standard environments configure both the API endpoint and customer-facing receipt origin:

| Environment | API endpoint | Receipt origin |
|  --- | --- | --- |
| `CheqiApiEnvironment.Sandbox` | `https://sandbox.api.cheqi.io` | `https://sandbox.receipt.cheqi.io` |
| `CheqiApiEnvironment.Test` | `https://test.api.cheqi.io` | `https://test.receipt.cheqi.io` |
| `CheqiApiEnvironment.Production` | `https://api.cheqi.io` | `https://receipt.cheqi.io` |


For a custom deployment, configure both URLs if you use download receipts:

```csharp
using var sdk = CheqiSdk.Create(new CheqiSdkOptions
{
    CustomApiEndpoint = "http://localhost:8080",
    ReceiptDownloadBaseUrl = "http://localhost:5190",
    ApiKey = System.Environment.GetEnvironmentVariable("CHEQI_API_KEY")
});
```

`CheqiSdkOptions` also accepts `PrivateKey`, `TimeoutSeconds`, `MaxRetries`, `LoggerFactory`, and an `HttpClient`. An injected `HttpClient` and `ILoggerFactory` remain owned by the caller; a client created internally is disposed with the SDK. A fluent `CheqiSdk.Builder()` is available as an alternative to `CheqiSdkOptions`.

## Request options and OAuth

Every HTTP operation takes an optional `RequestOptions` before its `CancellationToken`. 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.

```csharp
using Cheqi.Sdk.Http;

var options = RequestOptions.WithAccessToken(accessToken);

var result = await sdk.ReceiptService.IssueReceiptAsync(
    identification, payload, storeId,
    options: options,
    cancellationToken: cancellationToken);
```

The same `RequestOptions` can also set `Timeout` and `MaxRetries` for a single call, for example `new RequestOptions { AccessToken = accessToken, Timeout = TimeSpan.FromSeconds(60) }`.

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:

```csharp
using Cheqi.Sdk.Models.Generated;

var token = await sdk.OAuthService.RequestAccessTokenAsync(new ClientCredentialsTokenRequest
{
    ClientId = clientId,
    ClientSecret = clientSecret,
    GrantId = grantId,
    Scope = "receipt:issue"
}, cancellationToken: cancellationToken);

// token.AccessToken, token.ExpiresIn
```

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`. `RequestOptions.Timeout` applies per call; an injected `HttpClient` keeps its own `Timeout` as an upper bound. See [OAuth 2.0](/authentication/oauth).

## Issue a receipt

Use the generated models in `Cheqi.Sdk.Models.Generated`. They represent the published OpenAPI contract, including payment context, fiscalization and jurisdictional metadata, taxes, discounts, charges, and product identifiers.

```csharp
using Cheqi.Sdk.Models.Generated;

var identification = new IdentificationDetails
{
    PaymentType = PaymentType.CARDPAYMENT,
    CardDetails = new CardDetails("PAR123456789") { CardProvider = CardDetails.CardProviderEnum.VISA },
    RecipientEmail = "customer@example.com"
};

var product = new Product("Coffee beans", "SKU-COFFEE-001", 1, UnitCode.C62, 10.00m, 10.00m, 12.10m)
{
    BrandName = "Cheqi Coffee",
    Taxes = new List<Tax> { new(21, "VAT", 10.00m) { Amount = 2.10m } }
};

var payload = new ReceiptPayload(
    documentNumber: "POS-2026-0001",
    issueDate: DateTimeOffset.UtcNow,
    currency: "EUR",
    receiptSubtotal: 10.00m,
    totalBeforeTax: 10.00m,
    totalTaxAmount: 2.10m,
    totalAmount: 12.10m,
    taxesApplied: true,
    products: new List<Product> { product })
{
    Taxes = new List<Tax> { new(21, "VAT", 10.00m) { Amount = 2.10m, Label = "VAT 21%" } },
    PaymentDetails = new PaymentDetails
    {
        PaymentMeansCode = "48",
        Description = "Card payment",
        CardProvider = "VISA",
        CardLastFour = "4242",
        MerchantId = "MID-123",
        PaymentTerminalId = "TID-456"
    }
};

var result = await sdk.ReceiptService.IssueReceiptAsync(
    identification, payload, cancellationToken: cancellationToken);

if (result.IsAccepted)
{
    Console.WriteLine($"Receipt accepted: {result.CheqiReceiptId} ({result.DeliveryRouteType})");
}
else if (result.DownloadEnvelopeRequired)
{
    // 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:

```csharp
var qrIdentification = new IdentificationDetails { PaymentType = PaymentType.CARDPAYMENT };

var qrResult = await sdk.ReceiptService.IssueReceiptAsync(qrIdentification, payload);

if (qrResult.DownloadUrl is { } url)
{
    // 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).

### Store association

`IssueReceiptAsync` takes an optional `Guid? storeId` before the request options:

```csharp
// Configured API key
await sdk.ReceiptService.IssueReceiptAsync(identification, payload);

// Store ID and the access token of the connected company
await sdk.ReceiptService.IssueReceiptAsync(identification, payload, storeId,
    options: RequestOptions.WithAccessToken(accessToken));
```

## Delivery routes and fallbacks

`ReceiptResult.State` is the checkout outcome and `ReceiptResult.DeliveryRouteType` the selected route:

| State | Meaning | Integration action |
|  --- | --- | --- |
| **DigitalSubmitted** | Cheqi accepted the processing job | Save `CheqiReceiptId` and the submission |
| **DigitalPending** | Acceptance is uncertain or the original match is processing | Persist the prepared download, upload it, then resume the original match |
| **DigitalExpired** | An unsubmitted match expired | Follow your expired-checkout policy |
| **DownloadFallback** | Download route accepted, prepared for later upload, or awaiting an envelope | Check `IsAccepted`, `PreparedDownload`, and `DownloadEnvelopeRequired` |
| **EmailFallback** | Explicit email route | Generate the final receipt locally and send it through `SendReceiptViaEmailAsync` |


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`, `IssueReceiptAsync` throws `CheqiSdkException` with error code `CUSTOMER_NOT_FOUND`. Permanent API errors propagate as `CheqiApiException`; cancellation propagates as `OperationCanceledException`.

### Recover an uncertain digital submission

If `IssueReceiptAsync` returns `DigitalPending`, retain its `MatchId` and the same definitive `ReceiptPayload`. Persist the prepared download before showing its URL, then resume that match rather than resolving the customer and issuing a second receipt:

```csharp
using System.Text.Json;
using Cheqi.Sdk.Receipt;
using Cheqi.Sdk.Serialization;

if (result.PreparedDownload is { } prepared)
{
    // Persist this JSON with appropriate protection: its URL includes the secret key.
    var persistedJson = JsonSerializer.Serialize(prepared, CheqiJson.CreateOptions());
    await sdk.ReceiptService.UploadPreparedDownloadAsync(prepared, cancellationToken: cancellationToken);
}

if (result.State == ReceiptIssueState.DigitalPending)
{
    var resumed = await sdk.ReceiptService.ResumeReceiptAsync(
        result.MatchId!, payload, storeId, cancellationToken: cancellationToken);
}
```

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. Encrypted digital submissions are never retried automatically. See [Sending Receipts](/receipts/sending#submission-rules).

### Explicit download receipt

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

```csharp
var download = await sdk.ReceiptService.IssueDownloadReceiptAsync(
    new IdentificationDetails { PaymentType = PaymentType.CASH },
    payload,
    cancellationToken: cancellationToken);

Console.WriteLine(download.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:

```csharp
var completed = await sdk.ReceiptService.CompleteDownloadFallbackAsync(
    result, receiptEnvelope, templateHash, cancellationToken: cancellationToken);
```

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:

```csharp
var resolution = await sdk.MatchingService.MatchCustomerAsync(identification, options);

var envelope = sdk.ReceiptService.GenerateEncryptedReceiptEnvelope(resolution, payload, storeId);

var response = await sdk.ReceiptService.SubmitEncryptedReceiptAsync(envelope, options);
```

`GenerateEncryptedReceiptEnvelope` also accepts a retrieved `MATCHED` status from `GetMatchAsync`. The caller must honor the selected route and preserve identical definitive plaintext across device encryptions.

## Credit notes

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

```csharp
var creditNote = await sdk.CreditNoteService.IssueCreditNoteAsync(
    identification,
    parentCheqiReceiptId: "original-cheqi-receipt-id",
    creditNotePayload: definitiveCreditNotePayload,
    storeId: storeId,
    options: RequestOptions.WithAccessToken(accessToken),
    cancellationToken: cancellationToken);
```

The payload can be a serializable object, a `JsonElement`, or an already serialized JSON string; it is submitted unchanged 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:

```csharp
var request = sdk.DecryptionService.DecryptCreditNoteInitiationRequest(
    webhook.Data.CreditNoteInitiationRequest,
    privateKey);
```

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

```csharp
ReceiptEnvelope envelope = sdk.DecryptionService.DecryptReceipt(receiptDelivery, privateKey);
```

Private keys may be PKCS#8 PEM or base64 DER. 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:

```csharp
var verification = sdk.VerificationService;

string cheqiHash = verification.CalculateCheqiReceiptHash(cheqiReceiptJson);
string ublHash = verification.CalculateUblHash(ublPurchaseReceiptXml);

string canonicalJson = verification.CanonicalizeCheqiReceipt(cheqiReceiptJson);
string canonicalXml = verification.CanonicalizeUbl(ublPurchaseReceiptXml);
```

Use `CheqiJson.CreateOptions()` whenever you serialize generated models or persisted results, so wire enum values, explicit zero amounts, optional field presence, and date offsets are preserved.

## Store management

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

```csharp
var stores = sdk.StoreService;
var options = RequestOptions.WithAccessToken(accessToken);

StoreDTO store = await stores.CreateStoreAsync(companyId, createStoreRequest, options);
List<StoreDTO> allStores = await stores.GetStoresAsync(companyId, options: options);
List<StoreDTO> activeStores = await stores.GetActiveStoresAsync(companyId, options);
StoreDTO selected = await stores.GetStoreAsync(companyId, storeId, options);
StoreDTO updated = await stores.UpdateStoreAsync(companyId, storeId, createStoreRequest, options);

await stores.ActivateStoreAsync(companyId, storeId, options);
await stores.DeactivateStoreAsync(companyId, storeId, options);
await stores.DeleteStoreAsync(companyId, storeId, options);
```

## Errors

API failures throw `CheqiApiException`; SDK validation and routing failures throw `CheqiSdkException`. Both carry an `ErrorCode`, and API errors also carry the HTTP status and correlation ID where available:

```csharp
using Cheqi.Sdk.Http.Exceptions;

try
{
    await sdk.ReceiptService.IssueReceiptAsync(identification, payload, options: options);
}
catch (CheqiApiException exception)
{
    Console.Error.WriteLine($"{exception.Message} ({exception.ErrorCode}, HTTP {exception.HttpStatusCode})");
    if (exception.CorrelationId is { } correlationId)
    {
        Console.Error.WriteLine($"Correlation ID: {correlationId}");
    }
}
```

## Available services

`CheqiSdk` exposes the services and lower-level API client as properties:

- `ReceiptService`
- `CreditNoteService`
- `MatchingService`
- `EncryptionService`
- `DecryptionService`
- `DownloadService`
- `VerificationService`
- `StoreService`
- `CompanyService`
- `WebhookService`
- `OAuthService`
- `ApiClient`
- `Config`