Skip to content
Last updated

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.

Reject requests with invalid signature

If the signature does not match, the request may have been tampered with or sent by an unauthorized party. Reject it immediately.


Verification process

Where to find your API key

Go to API keys page of KYCAID Dashboard.

Step 1. Read the raw request body

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"}

Step 2. Encode the body using Base64

Example:

eyJyZXF1ZXN0X2lkIjoiNjFhN2RiY2MwMTJkOTA0MmU5MDljZjAwNmU3YjQxMmQ2YmE1IiwidHlwZSI6IlZFUklGSUNBVElPTl9TVEFUVVNfQ0hBTkdFRCIsImFwcGxpY2FudF9pZCI6IjQxNDFjYzFiMThkYmEwNDg0NzBiMjk2MWNiNDU5MmY0ODBmZSIsInZlcmlmaWNhdGlvbl9pZCI6IjJjZjc5NWU3MTNiZTEwNDBlNTBiMjAyMTY0ZWUxN2JmZGZiZSIsImZvcm1faWQiOiI1OGJlZDg3NjAwZGQ5OTQ0ZjAyYmEwYzljZDhiMzJkNmJkNGMiLCJ2ZXJpZmljYXRpb25fc3RhdHVzIjoicGVuZGluZyJ9

Step 3. Compute the HMAC-SHA512 hash

Use 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

Step 4. Compare with the x-data-integrity header

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.


Code examples

Node.js

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');
});

Python

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)

PHP

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);
}

Notes

Use constant-time comparison

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.

Read the raw body, not parsed JSON

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.

Respond promptly

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.

Reject invalid signatures

If the computed hash does not match the x-data-integrity header, return HTTP 401 or 403 and do not process the payload.