Setting up Webhooks#

Using Webhooks is an additional step for security, and to speed up the user journey. It also allows for a more efficient way of handling the results rather than polling. This means that the verification results are pushed to your server as soon as they are available.

The webhooks and result for each application can be shown in the Dashboard by clicking the application and looking at the Webhooks tab.

There are two ways to set up the webhooks, either Globally or per Flow.

Webhook Payload and Retries#

The Webhook payload follows the same structure as the results retrieved directly from the API. See Verification response for the JSON structure of the webhook content.

The webhook is sent as a POST request to the configured endpoint. The payload is sent as a JSON object in the body of the request. The headers contain the X-Signature and X-Timestamp for security.

The Webhook will be retried if the configured server does not respond with a 200 status code. The retry interval is 5 seconds. If the server does not respond with a 200 status code after 5 minutes, the webhook will be marked as failed.

The setting up of a webhook listener may require you to change firewall settings to allow incoming traffic from the Checkin.com servers. The Checkin.com IP addresses used to send out the webhooks are dynamic for the test/sandbox systems. For production a fixed IP range is used.

Global Webhooks#

Global webhooks provide event notifications for all verification processes, regardless of the Flow. This allows centralized monitoring of verification results.

Global webhooks are set up in the Dashboard under Settings → Webhooks. The endpoint URL is set up here, and the events that should trigger the webhook are selected. In case multiple endpoints are set up, the hook will be sent to all configured endpoints.

Webhooks Per Flow#

Webhooks can be set up for specific Flows to receive notifications about verification events in specific flows. These are useful when you need to get updates only for certain verification flows, or to different endpoints for different flows.

The Flow webhooks are set up in the Dashboard under Flows → [Flow] → Webhooks. To enable specific webhooks for a flow, the setting "Use own webhooks" on this page needs to be enabled and saved. Then specific webhooks can be added for this flow.

Signature Validation Example#

The Webhook payload is signed with a private key and the signature is sent in the X-Signature header. The signature is used to verify the authenticity of the payload. The public key is available in the Dashboard under Settings → Webhooks → "Signature key" to the top right. You need to copy the public Signature key to your code and use it to verify the signature.

A standard signing is used bu setting the X-Signature header. To verify it the HTTP body is concatenated with the X-Timestamp header as strings, and then verified with the obtained public key.

The EdDSA / Ed25519 https://en.wikipedia.org/wiki/EdDSA#Ed25519 algorithm is used to sign and verify the webhook's content. The key is in the "der" format.

Some programming languages already support the algorithm natively, such as Node.js, Go. Some programming languages have this algorithm inside the widely used libraries: PyPI https://pypi.org/project/cryptography/ , Python-Pynacl https://pynacl.readthedocs.io/en/latest/signing/#nacl.signing.VerifyKey , C https://doc.libsodium.org/public-key_cryptography/public-key_signatures.

Example: Validating Signature in Python:

from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
from nacl.encoding import RawEncoder
import base64
import re

def validate_webhook_signature(public_key: str, body_payload: str, timestamp: str, signature: str):
    try:
        # Extract the base64-encoded key content between the PEM headers
        key_base64 = re.search(r"-----BEGIN PUBLIC KEY-----\n(.*?)\n-----END PUBLIC KEY-----", public_key, re.DOTALL).group(1)
        public_key_bytes = base64.b64decode(key_base64)[12:] # skip the ASN.1 header for Ed25519 keys
        signature_bytes = base64.b64decode(signature)

        # Concatenate the payload and timestamp
        signed_message = (body_payload + timestamp).encode('utf-8')

        # Verify the signature
        verify_key = VerifyKey(public_key_bytes, encoder=RawEncoder)
        verify_key.verify(signed_message, signature_bytes)

    except (BadSignatureError, ValueError) as e:
        raise ValueError("Invalid signature.") from e

# Public Key from dashboard 
publicKey = '''-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAHjRvclLumSsWdlS6nILNavTV+wo6AE3eFm1SrlUZqq8=
-----END PUBLIC KEY-----'''
received_signature =  req.headers[0]["x-signature"] # like: 'Ut/IH+ye2u2CHs...mXYOgDA=='
timestamp = req.headers[0]["x-timestamp"]  #like: '1740991573771'
body_payload = req.body # like: '{"id":"67c56c55fd9e31d55a11e843","application":{"documents"....'

try:
    validate_webhook_signature(publicKey, body_payload, timestamp, received_signature)
    print("Signature is valid.")
except ValueError as e:
    print('Invalid signature')

This ensures that only legitimate webhook notifications are processed.

Capture webhook and validate Signature in NodeJS: Example of validating the signature using Node JS:


const express = require('express');
 const crypto = require('crypto');
 const app = express();
 app.use(express.urlencoded({ extended: false }));
 app.listen( 8000, () => {
    console.log( server started at port 8000 );
 });

 /* Signature key from settings page */

 const publicKey = '-----BEGIN PUBLIC KEY-----\n' +
 'MCowBQYDK2VwAyEAvGhtRZLKTfIWS/4K/P5tUvHeumX5l4MdwXdqYW0JxKk=\n' +
 '-----END PUBLIC KEY-----';

 app.post('/your_URL_to_handle_webhooks', async (req, res ) => {
     try {
         const timestamp = req.header('X-Timestamp');
         const signature = req.header('X-Signature');
         const msg = JSON.stringify(req.body).concat(timestamp);
         const valid = crypto.verify(
             null,
             Buffer.from(msg),
             publicKey,
             Buffer.from(signature, 'base64')
         );
         if(valid){
            // respond with 200
            // Save data from body
         } else {
            // signature is not valid, data received from unknown caller
            // Alert that someone is trying invalid requests
         }
     } catch (e) {
        // error
     }
 });