{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Webhook Security","description":"Complete documentation for integrating Cheqi's digital receipt platform","keywords":["cheqi","digital receipts","api","sdk","java","javascript"],"llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhook-security","__idx":0},"children":["Webhook Security"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Cheqi signs webhook requests with HMAC-SHA256 when a webhook secret is configured for the receiving company or client application."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"signature-format","__idx":1},"children":["Signature format"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Cheqi-Signature"]}," value has this form:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"text","header":{"controls":{"copy":{}}},"source":"sha256=BASE64_HMAC_DIGEST\n","lang":"text"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The digest is computed over the exact serialized JSON request body:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"text","header":{"controls":{"copy":{}}},"source":"Base64(HMAC-SHA256(webhookSecret, rawRequestBody))\n","lang":"text"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["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."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"verification-procedure","__idx":2},"children":["Verification procedure"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Read the raw HTTP request bytes without modifying them."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Read ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Cheqi-Signature"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compute HMAC-SHA256 using the webhook secret and raw body."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Base64-encode the digest and prefix it with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sha256="]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compare the supplied and expected signatures in constant time."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Reject invalid signatures before parsing or enqueuing the event."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"java-implementation","__idx":3},"children":["Java implementation"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"import java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.util.Base64;\nimport javax.crypto.Mac;\nimport javax.crypto.spec.SecretKeySpec;\n\npublic final class WebhookSignatureVerifier {\n    private final byte[] secret;\n\n    public WebhookSignatureVerifier(String webhookSecret) {\n        this.secret = webhookSecret.getBytes(StandardCharsets.UTF_8);\n    }\n\n    public boolean isValid(byte[] rawBody, String suppliedSignature) {\n        if (suppliedSignature == null) {\n            return false;\n        }\n\n        try {\n            Mac mac = Mac.getInstance(\"HmacSHA256\");\n            mac.init(new SecretKeySpec(secret, \"HmacSHA256\"));\n            String expected = \"sha256=\"\n                    + Base64.getEncoder().encodeToString(mac.doFinal(rawBody));\n\n            return MessageDigest.isEqual(\n                    expected.getBytes(StandardCharsets.US_ASCII),\n                    suppliedSignature.getBytes(StandardCharsets.US_ASCII)\n            );\n        } catch (Exception exception) {\n            throw new IllegalStateException(\"Cannot verify webhook signature\", exception);\n        }\n    }\n}\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Controller usage:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"java","header":{"controls":{"copy":{}}},"source":"@PostMapping(\"/webhooks/cheqi\")\npublic ResponseEntity<Void> receive(\n        @RequestBody byte[] rawBody,\n        @RequestHeader(\"X-Cheqi-Signature\") String signature\n) throws IOException {\n    if (!signatureVerifier.isValid(rawBody, signature)) {\n        return ResponseEntity.status(401).build();\n    }\n\n    WebhookEvent event = objectMapper.readValue(rawBody, WebhookEvent.class);\n    webhookQueue.enqueue(event);\n    return ResponseEntity.ok().build();\n}\n","lang":"java"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"nodejs-implementation","__idx":4},"children":["Node.js implementation"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"import crypto from \"node:crypto\";\nimport express from \"express\";\n\nfunction verifyCheqiSignature(rawBody, suppliedSignature, webhookSecret) {\n  if (!suppliedSignature) return false;\n\n  const expected =\n    \"sha256=\" +\n    crypto.createHmac(\"sha256\", webhookSecret).update(rawBody).digest(\"base64\");\n\n  const supplied = Buffer.from(suppliedSignature, \"ascii\");\n  const calculated = Buffer.from(expected, \"ascii\");\n\n  return (\n    supplied.length === calculated.length &&\n    crypto.timingSafeEqual(supplied, calculated)\n  );\n}\n\nconst app = express();\n\napp.post(\n  \"/webhooks/cheqi\",\n  express.raw({ type: \"application/json\" }),\n  async (req, res) => {\n    if (\n      !verifyCheqiSignature(\n        req.body,\n        req.header(\"X-Cheqi-Signature\"),\n        process.env.CHEQI_WEBHOOK_SECRET\n      )\n    ) {\n      return res.sendStatus(401);\n    }\n\n    await webhookQueue.enqueue(JSON.parse(req.body.toString(\"utf8\")));\n    return res.sendStatus(200);\n  }\n);\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"secret-handling","__idx":5},"children":["Secret handling"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Store the webhook secret in a managed secret store or environment variable."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Never commit it to source control."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Never log the secret, the signature input, unwrapped AES keys, or decrypted documents."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Rotate a compromised secret and update every receiver atomically."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Use separate secrets and endpoints for test, sandbox, and production."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"encrypted-payload-handling","__idx":6},"children":["Encrypted payload handling"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["HMAC verification authenticates the webhook transport. Receipt and credit-note contents remain protected separately by recipient encryption."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RECEIPT_CREATED"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CREDIT_NOTE_CREATED"]},":"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Select the private key corresponding to the event's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["publicKey"]}," snapshot."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["recipientKeyAlgorithm"]}," and the SDK to unwrap ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["encryptedEnvelopeKey"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Decrypt ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["encryptedEnvelope"]}," into ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ReceiptEnvelope"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Treat all ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ReceiptEnvelope.documents"]}," content as sensitive."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Keep historical private keys available for receipts encrypted before rotation."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["For ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["RETURN_REQUESTED"]},", decrypt ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["encryptedCreditNoteInitiationRequest"]}," with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["encryptedSymmetricKey"]}," and deserialize the result as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CreditNoteInitiationRequest"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The webhook never includes plaintext receipt documents, plaintext return requests, or separate encrypted customer details."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"endpoint-hardening","__idx":7},"children":["Endpoint hardening"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Require HTTPS."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Limit accepted methods and content types."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Apply a reasonable body-size limit before buffering."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Rate-limit invalid requests without blocking legitimate retry bursts."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Respond with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["401"]}," for invalid signatures and do not disclose comparison details."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Persist accepted events before returning ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Make downstream processing idempotent."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Monitor signature failures, non-2xx responses, and processing backlogs."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["IP allowlists can be used as defense in depth, but they do not replace HMAC verification."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"security-checklist","__idx":8},"children":["Security checklist"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Verify ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Cheqi-Signature"]}," against the exact raw body."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Use constant-time comparison."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Keep secrets out of code and logs."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Use HTTPS in every environment."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Enforce request-size and rate limits."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Keep historical envelope-decryption keys safely available."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Never log decrypted ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ReceiptEnvelope"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CreditNoteInitiationRequest"]}," contents."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Process retries idempotently."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/webhooks/events"},"children":["Webhook Events"]}," for the signed payload shapes and ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/webhooks/receipt-webhooks"},"children":["Receipt Webhooks"]}," for ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ReceiptEnvelope"]}," processing."]}]},"headings":[{"value":"Webhook Security","id":"webhook-security","depth":1},{"value":"Signature format","id":"signature-format","depth":2},{"value":"Verification procedure","id":"verification-procedure","depth":2},{"value":"Java implementation","id":"java-implementation","depth":2},{"value":"Node.js implementation","id":"nodejs-implementation","depth":2},{"value":"Secret handling","id":"secret-handling","depth":2},{"value":"Encrypted payload handling","id":"encrypted-payload-handling","depth":2},{"value":"Endpoint hardening","id":"endpoint-hardening","depth":2},{"value":"Security checklist","id":"security-checklist","depth":2}],"frontmatter":{"seo":{"title":"Webhook Security"}},"lastModified":"2026-08-03T14:08:42.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/webhooks/security","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}