Skip to content

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:

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"
    ]
  }'
PropertyRequiredDescription
nameYesHuman-readable subscription name.
notificationUrlYesPublic HTTPS endpoint that accepts Cheqi webhook requests.
eventsYesEvent 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:

{
  "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 for the complete shapes.

Requests include these headers:

HeaderDescription
Content-Typeapplication/json.
User-AgentCheqi webhook service identifier.
X-Cheqi-Event-TypeEvent type, also present in the JSON body.
X-Cheqi-Signaturesha256= 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

@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

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:

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

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.