Every callback request from KYCAID includes a cryptographic signature in the x-data-integrity HTTP header. You must verify this signature before processing the request to ensure authenticity and data integrity.
If the signature does not match, the request may have been tampered with or sent by an unauthorized party. Reject it immediately.
Go to API keys page of KYCAID Dashboard.
Read the incoming request body as a raw byte sequence (UTF-8). Do not parse it as JSON at this stage.
Example:
{"request_id":"61a7dbcc012d9042e909cf006e7b412d6ba5","type":"VERIFICATION_STATUS_CHANGED","applicant_id":"4141cc1b18dba048470b2961cb4592f480fe","verification_id":"2cf795e713be1040e50b202164ee17bfdfbe","form_id":"58bed87600dd9944f02ba0c9cd8b32d6bd4c","verification_status":"pending"}Example:
eyJyZXF1ZXN0X2lkIjoiNjFhN2RiY2MwMTJkOTA0MmU5MDljZjAwNmU3YjQxMmQ2YmE1IiwidHlwZSI6IlZFUklGSUNBVElPTl9TVEFUVVNfQ0hBTkdFRCIsImFwcGxpY2FudF9pZCI6IjQxNDFjYzFiMThkYmEwNDg0NzBiMjk2MWNiNDU5MmY0ODBmZSIsInZlcmlmaWNhdGlvbl9pZCI6IjJjZjc5NWU3MTNiZTEwNDBlNTBiMjAyMTY0ZWUxN2JmZGZiZSIsImZvcm1faWQiOiI1OGJlZDg3NjAwZGQ5OTQ0ZjAyYmEwYzljZDhiMzJkNmJkNGMiLCJ2ZXJpZmljYXRpb25fc3RhdHVzIjoicGVuZGluZyJ9Use your API key as the secret key and the Base64-encoded string from Step 2 as the input.
Example:
- Secret key (API token):
28c6f7cc0345a04eee0b535039b1c5a62547 - Result (lowercase hex):
f7681b097b77928fc031d614709976796057c306cf77fdd449bb414937bd87678d908d7efaa65e9b1dd65b9eeea2121ea75bd9007f44fe8fcd7c9ac6cdeeef0e
Compare the computed hash with the value from the 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.
const crypto = require('crypto');
function verifyCallback(rawBody, xDataIntegrity, apiToken) {
const base64Body = Buffer.from(rawBody, 'utf-8').toString('base64');
const hash = crypto
.createHmac('sha512', apiToken)
.update(base64Body)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
const hashBuffer = Buffer.from(hash, 'utf-8');
const headerBuffer = Buffer.from(xDataIntegrity, 'utf-8');
if (hashBuffer.length !== headerBuffer.length) {
return false;
}
return crypto.timingSafeEqual(hashBuffer, headerBuffer);
}
// Usage in an Express handler
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyCallback(
req.body,
req.headers['x-data-integrity'],
process.env.KYCAID_API_TOKEN
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body);
// Process the callback...
res.status(200).send('OK');
});import base64
import hashlib
import hmac
def verify_callback(raw_body: bytes, x_data_integrity: str, api_token: str) -> bool:
base64_body = base64.b64encode(raw_body).decode('utf-8')
computed_hash = hmac.new(
api_token.encode('utf-8'),
base64_body.encode('utf-8'),
hashlib.sha512
).hexdigest()
# Use hmac.compare_digest to prevent timing attacks
return hmac.compare_digest(computed_hash, x_data_integrity)function verifyCallback(string $rawBody, string $xDataIntegrity, string $apiToken): bool
{
$base64Body = base64_encode($rawBody);
$hash = hash_hmac('sha512', $base64Body, $apiToken);
// Use hash_equals to prevent timing attacks
return hash_equals($hash, $xDataIntegrity);
}Never compare signatures with == or ===. 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.
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.
Return an HTTP 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.
If the computed hash does not match the x-data-integrity header, return HTTP 401 or 403 and do not process the payload.