Register an HTTPS endpoint to receive Cheqi receipt, credit-note, and return-request events.
- 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.
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"
]
}'| 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.
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:
| 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. |
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.
@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();
}
}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 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.
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 contextThe 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.
- Start your webhook endpoint locally.
- Expose it through an HTTPS tunnel.
- Register the tunnel URL as
notificationUrlin a non-production Cheqi environment. - Trigger a real event in that environment.
- 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.
- 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.
- Return a
2xxresponse promptly. - Persist first and process asynchronously.
- Inspect your endpoint latency and non-2xx responses.
- 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.