{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":["admonition"]},"type":"markdown"},"seo":{"title":"Callback integrity verification","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":"callback-integrity-verification","__idx":0},"children":["Callback integrity verification"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Every callback request from KYCAID includes a cryptographic signature in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-data-integrity"]}," HTTP header. You must verify this signature before processing the request to ensure authenticity and data integrity."]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"warning","name":"Reject requests with invalid signature"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If the signature does not match, the request may have been tampered with or sent by an unauthorized party. Reject it immediately."]}]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"verification-process","__idx":1},"children":["Verification process"]},{"$$mdtype":"Tag","name":"Admonition","attributes":{"type":"info","name":"Where to find your API key"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Go to ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://app.kycaid.com/api-keys"},"children":["API keys"]}," page of KYCAID Dashboard."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-1-read-the-raw-request-body","__idx":2},"children":["Step 1. Read the raw request body"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Read the incoming request body as a raw byte sequence (UTF-8). Do not parse it as JSON at this stage."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Example"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\"request_id\":\"61a7dbcc012d9042e909cf006e7b412d6ba5\",\"type\":\"VERIFICATION_STATUS_CHANGED\",\"applicant_id\":\"4141cc1b18dba048470b2961cb4592f480fe\",\"verification_id\":\"2cf795e713be1040e50b202164ee17bfdfbe\",\"form_id\":\"58bed87600dd9944f02ba0c9cd8b32d6bd4c\",\"verification_status\":\"pending\"}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-2-encode-the-body-using-base64","__idx":3},"children":["Step 2. Encode the body using Base64"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Example"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"eyJyZXF1ZXN0X2lkIjoiNjFhN2RiY2MwMTJkOTA0MmU5MDljZjAwNmU3YjQxMmQ2YmE1IiwidHlwZSI6IlZFUklGSUNBVElPTl9TVEFUVVNfQ0hBTkdFRCIsImFwcGxpY2FudF9pZCI6IjQxNDFjYzFiMThkYmEwNDg0NzBiMjk2MWNiNDU5MmY0ODBmZSIsInZlcmlmaWNhdGlvbl9pZCI6IjJjZjc5NWU3MTNiZTEwNDBlNTBiMjAyMTY0ZWUxN2JmZGZiZSIsImZvcm1faWQiOiI1OGJlZDg3NjAwZGQ5OTQ0ZjAyYmEwYzljZDhiMzJkNmJkNGMiLCJ2ZXJpZmljYXRpb25fc3RhdHVzIjoicGVuZGluZyJ9\n"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-3-compute-the-hmac-sha512-hash","__idx":4},"children":["Step 3. Compute the HMAC-SHA512 hash"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use your ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API key"]}," as the secret key and the Base64-encoded string from Step 2 as the input."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Example"]},":"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Secret key (API token): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["28c6f7cc0345a04eee0b535039b1c5a62547"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Result (lowercase hex): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["f7681b097b77928fc031d614709976796057c306cf77fdd449bb414937bd87678d908d7efaa65e9b1dd65b9eeea2121ea75bd9007f44fe8fcd7c9ac6cdeeef0e"]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"step-4-compare-with-the-x-data-integrity-header","__idx":5},"children":["Step 4. Compare with the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-data-integrity"]}," header"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Compare the computed hash with the value from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-data-integrity"]}," request header using a constant-time comparison function. If the two values are equal, the request is authentic. Otherwise, reject the request immediately."]},{"$$mdtype":"Tag","name":"hr","attributes":{},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"code-examples","__idx":6},"children":["Code examples"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"nodejs","__idx":7},"children":["Node.js"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const crypto = require('crypto');\n\nfunction verifyCallback(rawBody, xDataIntegrity, apiToken) {\n  const base64Body = Buffer.from(rawBody, 'utf-8').toString('base64');\n  const hash = crypto\n    .createHmac('sha512', apiToken)\n    .update(base64Body)\n    .digest('hex');\n\n  // Use timingSafeEqual to prevent timing attacks\n  const hashBuffer = Buffer.from(hash, 'utf-8');\n  const headerBuffer = Buffer.from(xDataIntegrity, 'utf-8');\n\n  if (hashBuffer.length !== headerBuffer.length) {\n    return false;\n  }\n\n  return crypto.timingSafeEqual(hashBuffer, headerBuffer);\n}\n\n// Usage in an Express handler\napp.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {\n  const isValid = verifyCallback(\n    req.body,\n    req.headers['x-data-integrity'],\n    process.env.KYCAID_API_TOKEN\n  );\n\n  if (!isValid) {\n    return res.status(401).send('Invalid signature');\n  }\n\n  const payload = JSON.parse(req.body);\n  // Process the callback...\n  res.status(200).send('OK');\n});\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"python","__idx":8},"children":["Python"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import base64\nimport hashlib\nimport hmac\n\ndef verify_callback(raw_body: bytes, x_data_integrity: str, api_token: str) -> bool:\n    base64_body = base64.b64encode(raw_body).decode('utf-8')\n    computed_hash = hmac.new(\n        api_token.encode('utf-8'),\n        base64_body.encode('utf-8'),\n        hashlib.sha512\n    ).hexdigest()\n\n    # Use hmac.compare_digest to prevent timing attacks\n    return hmac.compare_digest(computed_hash, x_data_integrity)\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"php","__idx":9},"children":["PHP"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"php","header":{"controls":{"copy":{}}},"source":"function verifyCallback(string $rawBody, string $xDataIntegrity, string $apiToken): bool\n{\n    $base64Body = base64_encode($rawBody);\n    $hash = hash_hmac('sha512', $base64Body, $apiToken);\n\n    // Use hash_equals to prevent timing attacks\n    return hash_equals($hash, $xDataIntegrity);\n}\n","lang":"php"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"notes","__idx":10},"children":["Notes"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"use-constant-time-comparison","__idx":11},"children":["Use constant-time comparison"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Never compare signatures with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["=="]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["==="]},". Simple string comparison is vulnerable to timing attacks, where an attacker can infer the correct hash byte by byte based on response time differences. Always use the language-specific constant-time functions shown in the examples above."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"read-the-raw-body-not-parsed-json","__idx":12},"children":["Read the raw body, not parsed JSON"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Parsing and re-serializing JSON may alter whitespace or key order, producing a different Base64 value. Make sure your framework gives you access to the original request bytes before any parsing occurs."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"respond-promptly","__idx":13},"children":["Respond promptly"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Return an HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200 OK"]}," as soon as you have validated the signature and queued the event for processing. If KYCAID does not receive a timely response, the callback may be retried."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"reject-invalid-signatures","__idx":14},"children":["Reject invalid signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If the computed hash does not match the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-data-integrity"]}," header, return HTTP ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["401"]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["403"]}," and do not process the payload."]}]},"headings":[{"value":"Callback integrity verification","id":"callback-integrity-verification","depth":1},{"value":"Verification process","id":"verification-process","depth":2},{"value":"Step 1. Read the raw request body","id":"step-1-read-the-raw-request-body","depth":3},{"value":"Step 2. Encode the body using Base64","id":"step-2-encode-the-body-using-base64","depth":3},{"value":"Step 3. Compute the HMAC-SHA512 hash","id":"step-3-compute-the-hmac-sha512-hash","depth":3},{"value":"Step 4. Compare with the x-data-integrity header","id":"step-4-compare-with-the-x-data-integrity-header","depth":3},{"value":"Code examples","id":"code-examples","depth":2},{"value":"Node.js","id":"nodejs","depth":3},{"value":"Python","id":"python","depth":3},{"value":"PHP","id":"php","depth":3},{"value":"Notes","id":"notes","depth":2},{"value":"Use constant-time comparison","id":"use-constant-time-comparison","depth":3},{"value":"Read the raw body, not parsed JSON","id":"read-the-raw-body-not-parsed-json","depth":3},{"value":"Respond promptly","id":"respond-promptly","depth":3},{"value":"Reject invalid signatures","id":"reject-invalid-signatures","depth":3}],"frontmatter":{"seo":{"title":"Callback integrity verification"}},"lastModified":"2026-05-06T07:28:50.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/callbacks/callback-integrity-verification","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}