# Webhook Events

Every Cheqi webhook has an `event` and a `data` property. The key inside `data` identifies the object delivered by that event.

## Common structure

```json
{
  "event": "EVENT_NAME",
  "data": {
    "eventSpecificObject": {}
  }
}
```

The event-specific object may contain this routing metadata:

| Field | Description |
|  --- | --- |
| `clientId` | Public OAuth client ID for the webhook subscription, when owned by a client application. |
| `companyId` | Customer-company context, when applicable. |
| `userId` | Customer-user context, when applicable. |
| `created_at` | Time Cheqi created the webhook event. |
| `cheqiReceiptId` | Cheqi identifier of the receipt or credit note. |


Null and empty fields are omitted. Do not assume both `companyId` and `userId` are present.

## `RECEIPT_CREATED`

`data.encryptedReceipt` contains the encrypted document envelope produced by the elected owner device for your integration.

```json
{
  "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..."
    }
  }
}
```

Use `publicKey` and `recipientKeyAlgorithm` to select the corresponding private key and unwrap `encryptedEnvelopeKey`. Decrypt `encryptedEnvelope`, then deserialize its UTF-8 plaintext as a [`ReceiptEnvelope`](#receiptenvelope).

`finalHash` is the definitive CHEQI document hash submitted by the elected device together with the encrypted deliveries. There is no separate receipt-finalization webhook.

## `RETURN_REQUESTED`

The customer's device encrypts a `CreditNoteInitiationRequest` for the original issuer. Cheqi routes that ciphertext without decrypting it.

```json
{
  "event": "RETURN_REQUESTED",
  "data": {
    "creditNoteInitiationRequest": {
      "clientId": "issuer-public-client-id",
      "companyId": "550e8400-e29b-41d4-a716-446655440000",
      "userId": "8ccf09a7-d969-4a09-880f-42d520a0999a",
      "created_at": "2026-08-03T14:00:00Z",
      "cheqiReceiptId": "CHQ-20260803-ABC123",
      "publicKey": "base64-issuer-public-key...",
      "encryptedCreditNoteInitiationRequest": "base64-ciphertext...",
      "encryptedSymmetricKey": "base64-wrapped-aes-key..."
    }
  }
}
```

After decryption, the plaintext is a `CreditNoteInitiationRequest` containing the original receipt identifiers, requested return line items, the customer's note, and refund preference. It is not a `ReceiptEnvelope`.

## `CREDIT_NOTE_CREATED`

Credit notes use the same encrypted document-bundle contract as receipts. The event-specific key is `encryptedCreditNote`.

```json
{
  "event": "CREDIT_NOTE_CREATED",
  "data": {
    "encryptedCreditNote": {
      "clientId": "your-public-client-id",
      "companyId": "550e8400-e29b-41d4-a716-446655440000",
      "created_at": "2026-08-03T15:00:00Z",
      "cheqiReceiptId": "CHQ-CN-20260803-DEF456",
      "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-credit-note-hash..."
    }
  }
}
```

Decrypt `encryptedEnvelope` as a `ReceiptEnvelope`. Its `documents` map contains `CHEQI` and, when requested by the recipient, `UBL_CREDIT_NOTE`.

`finalHash` is the definitive CHEQI credit-note hash submitted by the elected device together with the encrypted deliveries. There is no separate credit-note finalization webhook.

## `ReceiptEnvelope`

The plaintext obtained by decrypting a receipt or credit-note `encryptedEnvelope` has this versioned structure:

```json
{
  "envelopeVersion": 1,
  "receiptUuid": "6f9619ff-8b86-d011-b42d-00cf4fc964ff",
  "cheqiReceiptId": "CHQ-20260803-ABC123",
  "receiptGeneratorVersion": "0.3.0",
  "documents": {
    "CHEQI": {
      "mediaType": "application/json",
      "content": "{...}"
    },
    "UBL_INVOICE": {
      "mediaType": "application/xml",
      "content": "<Invoice>...</Invoice>"
    }
  }
}
```

| Property | Description |
|  --- | --- |
| `envelopeVersion` | Version of the encrypted plaintext-envelope contract. |
| `receiptUuid` | Shared UUID for the locally generated document bundle. |
| `cheqiReceiptId` | Cheqi receipt or credit-note identifier. |
| `receiptGeneratorVersion` | Rust receipt-engine version used to generate the documents. |
| `documents` | Map keyed by `CHEQI`, `UBL_INVOICE`, `UBL_PURCHASE_RECEIPT`, or `UBL_CREDIT_NOTE`. |


Each document has a `mediaType` and string `content`. The keys actually present in `documents` are authoritative; there is no separate `receiptFormats` property in the webhook.

The envelope already contains the complete generated documents, including their supplier, customer, and payment context. There is no separate `encryptedCustomerDetails` or `encryptedCustomerAesKey` payload.

### Processing steps

1. Verify the webhook signature against the raw HTTP body.
2. Persist the event or establish idempotency before returning success.
3. Select the private key corresponding to the supplied public-key snapshot.
4. Unwrap the AES key from `encryptedEnvelopeKey` according to `recipientKeyAlgorithm`.
5. Decrypt `encryptedEnvelope` using the envelope encryption implementation in the Cheqi SDK.
6. Deserialize the plaintext as `ReceiptEnvelope`.
7. Read the required representation from `ReceiptEnvelope.documents`.
8. Canonicalize and hash the CHEQI document when independently verifying `finalHash`.


Do not log plaintext
Receipt and return contents can contain personal and financial data. Do not log ciphertext keys, decrypted envelopes, or document contents.

## Idempotency and retries

Cheqi can retry a webhook when delivery fails. Process events idempotently. A practical idempotency key is `event + cheqiReceiptId`; if your application can receive the same receipt for multiple authorization contexts, include the applicable `clientId`, `companyId`, or `userId` as well.

Always return `2xx` only after the raw event has been durably accepted. Perform decryption and downstream synchronization asynchronously.