# Webhook Setup

Register an HTTPS endpoint to receive Cheqi receipt, credit-note, and return-request events.

## Prerequisites

- A company or client-application access token with `read_receipts`.
- A publicly reachable HTTPS endpoint.
- A webhook secret configured for the subscription owner.
- Durable asynchronous processing and idempotency.


## Register a subscription

Create subscriptions with `POST /webhook/subscription`:

```bash
curl --request POST 'https://api.cheqi.io/webhook/subscription' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "name": "Production webhook",
    "notificationUrl": "https://your-domain.example/webhooks/cheqi",
    "events": [
      "RECEIPT_CREATED",
      "RETURN_REQUESTED",
      "CREDIT_NOTE_CREATED"
    ]
  }'
```

| Property | Required | Description |
|  --- | --- | --- |
| `name` | Yes | Human-readable subscription name. |
| `notificationUrl` | Yes | Public HTTPS endpoint that accepts Cheqi webhook requests. |
| `events` | Yes | Event types to subscribe to. |


Cheqi creates one subscription per requested event type. Existing active subscriptions for the same owner and event are not duplicated.

## Request format

Cheqi sends JSON with two top-level properties:

```json
{
  "event": "RECEIPT_CREATED",
  "data": {
    "encryptedReceipt": {
      "cheqiReceiptId": "CHQ-20260803-ABC123",
      "encryptedEnvelope": "base64-ciphertext...",
      "encryptedEnvelopeKey": "base64-wrapped-key..."
    }
  }
}
```

The object inside `data` depends on `event`. See [Webhook Events](/webhooks/events) for the complete shapes.

Requests include these headers:

| Header | Description |
|  --- | --- |
| `Content-Type` | `application/json`. |
| `User-Agent` | Cheqi webhook service identifier. |
| `X-Cheqi-Event-Type` | Event type, also present in the JSON body. |
| `X-Cheqi-Signature` | `sha256=` followed by the Base64 HMAC-SHA256 signature when a secret is configured. |


## Implement the endpoint

Signature verification must use the exact raw HTTP bytes. Do not bind the body to an object before verifying the signature, because parsing and re-serialization can change the byte sequence.

### Java example

```java
@RestController
@RequestMapping("/webhooks")
public class CheqiWebhookController {
    private final WebhookSignatureVerifier signatureVerifier;
    private final ObjectMapper objectMapper;
    private final WebhookQueue webhookQueue;

    @PostMapping("/cheqi")
    public ResponseEntity<Void> receive(
            @RequestBody byte[] rawBody,
            @RequestHeader("X-Cheqi-Signature") String signature
    ) throws IOException {
        if (!signatureVerifier.isValid(rawBody, signature)) {
            return ResponseEntity.status(401).build();
        }

        WebhookEvent event = objectMapper.readValue(rawBody, WebhookEvent.class);
        webhookQueue.enqueue(event, rawBody);
        return ResponseEntity.ok().build();
    }
}
```

### Node.js example

```javascript
import express from "express";

const app = express();

app.post(
  "/webhooks/cheqi",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.header("X-Cheqi-Signature");

    if (!verifyCheqiSignature(req.body, signature)) {
      return res.sendStatus(401);
    }

    const event = JSON.parse(req.body.toString("utf8"));
    await webhookQueue.enqueue(event, req.body);
    return res.sendStatus(200);
  }
);
```

## Route events

Route on `event`, then select the matching object in `data`:

```java
switch (event.event()) {
    case RECEIPT_CREATED -> processReceipt(event.data().encryptedReceipt());
    case RETURN_REQUESTED -> processReturn(event.data().creditNoteInitiationRequest());
    case CREDIT_NOTE_CREATED -> processCreditNote(event.data().encryptedCreditNote());
}
```

For encrypted receipt and credit-note events, decrypt `encryptedEnvelope` into a `ReceiptEnvelope` and read its `documents` map. See [Receipt Webhooks](/webhooks/receipt-webhooks) and [Return Webhooks](/webhooks/return-webhooks).

## Idempotency

Webhook delivery is at least once. A retry contains the same serialized payload recorded for the initial attempt. Establish idempotency before performing downstream writes.

A practical key is:

```text
event + cheqiReceiptId + authorization context
```

The authorization context can be the applicable `clientId`, `companyId`, or `userId` from the event-specific object.

Return `2xx` after the event is durably accepted, not after all decryption and downstream synchronization has finished.

## Local testing

1. Start your webhook endpoint locally.
2. Expose it through an HTTPS tunnel.
3. Register the tunnel URL as `notificationUrl` in a non-production Cheqi environment.
4. Trigger a real event in that environment.
5. Capture the exact raw body and signature header for repeatable signature tests.


Do not use fabricated plaintext receipt contents to test production flows. Test encryption and decryption with the same SDK envelope implementation used by your integration.

## Troubleshooting

### Signature verification fails

- Verify before JSON parsing.
- Hash the exact raw bytes, including whitespace.
- Remove the `sha256=` prefix only when comparing the decoded digest rather than the complete header value.
- Confirm the secret belongs to the subscription owner that received the event.


### Events are retried

- Return a `2xx` response promptly.
- Persist first and process asynchronously.
- Inspect your endpoint latency and non-2xx responses.


### Envelope decryption fails

- Select the private key matching the webhook's public-key snapshot.
- Respect `recipientKeyAlgorithm`.
- Do not Base64-decode or transform the ciphertext more than required by the SDK.
- Keep historical private keys available after key rotation.


Continue with [Webhook Security](/webhooks/security).