Cheqi signs webhook requests with HMAC-SHA256 when a webhook secret is configured for the receiving company or client application.
The X-Cheqi-Signature value has this form:
sha256=BASE64_HMAC_DIGESTThe digest is computed over the exact serialized JSON request body:
Base64(HMAC-SHA256(webhookSecret, rawRequestBody))Verify the signature before parsing JSON. Parsing and re-serializing the body can change whitespace, property ordering, escaping, or omitted values and will produce a different digest.
- Read the raw HTTP request bytes without modifying them.
- Read
X-Cheqi-Signature. - Compute HMAC-SHA256 using the webhook secret and raw body.
- Base64-encode the digest and prefix it with
sha256=. - Compare the supplied and expected signatures in constant time.
- Reject invalid signatures before parsing or enqueuing the event.
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class WebhookSignatureVerifier {
private final byte[] secret;
public WebhookSignatureVerifier(String webhookSecret) {
this.secret = webhookSecret.getBytes(StandardCharsets.UTF_8);
}
public boolean isValid(byte[] rawBody, String suppliedSignature) {
if (suppliedSignature == null) {
return false;
}
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret, "HmacSHA256"));
String expected = "sha256="
+ Base64.getEncoder().encodeToString(mac.doFinal(rawBody));
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.US_ASCII),
suppliedSignature.getBytes(StandardCharsets.US_ASCII)
);
} catch (Exception exception) {
throw new IllegalStateException("Cannot verify webhook signature", exception);
}
}
}Controller usage:
@PostMapping("/webhooks/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);
return ResponseEntity.ok().build();
}import crypto from "node:crypto";
import express from "express";
function verifyCheqiSignature(rawBody, suppliedSignature, webhookSecret) {
if (!suppliedSignature) return false;
const expected =
"sha256=" +
crypto.createHmac("sha256", webhookSecret).update(rawBody).digest("base64");
const supplied = Buffer.from(suppliedSignature, "ascii");
const calculated = Buffer.from(expected, "ascii");
return (
supplied.length === calculated.length &&
crypto.timingSafeEqual(supplied, calculated)
);
}
const app = express();
app.post(
"/webhooks/cheqi",
express.raw({ type: "application/json" }),
async (req, res) => {
if (
!verifyCheqiSignature(
req.body,
req.header("X-Cheqi-Signature"),
process.env.CHEQI_WEBHOOK_SECRET
)
) {
return res.sendStatus(401);
}
await webhookQueue.enqueue(JSON.parse(req.body.toString("utf8")));
return res.sendStatus(200);
}
);- Store the webhook secret in a managed secret store or environment variable.
- Never commit it to source control.
- Never log the secret, the signature input, unwrapped AES keys, or decrypted documents.
- Rotate a compromised secret and update every receiver atomically.
- Use separate secrets and endpoints for test, sandbox, and production.
HMAC verification authenticates the webhook transport. Receipt and credit-note contents remain protected separately by recipient encryption.
For RECEIPT_CREATED and CREDIT_NOTE_CREATED:
- Select the private key corresponding to the event's
publicKeysnapshot. - Use
recipientKeyAlgorithmand the SDK to unwrapencryptedEnvelopeKey. - Decrypt
encryptedEnvelopeintoReceiptEnvelope. - Treat all
ReceiptEnvelope.documentscontent as sensitive. - Keep historical private keys available for receipts encrypted before rotation.
For RETURN_REQUESTED, decrypt encryptedCreditNoteInitiationRequest with encryptedSymmetricKey and deserialize the result as CreditNoteInitiationRequest.
The webhook never includes plaintext receipt documents, plaintext return requests, or separate encrypted customer details.
- Require HTTPS.
- Limit accepted methods and content types.
- Apply a reasonable body-size limit before buffering.
- Rate-limit invalid requests without blocking legitimate retry bursts.
- Respond with
401for invalid signatures and do not disclose comparison details. - Persist accepted events before returning
2xx. - Make downstream processing idempotent.
- Monitor signature failures, non-2xx responses, and processing backlogs.
IP allowlists can be used as defense in depth, but they do not replace HMAC verification.
- Verify
X-Cheqi-Signatureagainst the exact raw body. - Use constant-time comparison.
- Keep secrets out of code and logs.
- Use HTTPS in every environment.
- Enforce request-size and rate limits.
- Keep historical envelope-decryption keys safely available.
- Never log decrypted
ReceiptEnvelopeorCreditNoteInitiationRequestcontents. - Process retries idempotently.
See Webhook Events for the signed payload shapes and Receipt Webhooks for ReceiptEnvelope processing.