Verification Results

Retrieve and Understand the Result

The result of the verification needs to be collected on the server side. The result is available in the Dashboard, via the API, or via Webhooks.

For testing it may be easier to use the API to retrieve the results by querying the status of an applicationID that has completed in the UI.

The data returned and its JSON structure is the same no matter if you choose to use the API or webhooks.

Use API to retrieve results

A simple way of getting the result is done by querying for the status of the applicationID. This will return a JSON object with the status of the verification.

curl --location 'https://[PARTNER].sb.getid.dev/api/v1/application/{ApplicationID}' \
--header 'X-API-Key: {YourAPIKey}' \
--header 'Content-Type: application/json'

Verification response

The response retrieves all information gathered from the application. Notable fields for processing are:

  • processingState: The state of the verification process as a string, either done or processing. If the state is processing, the final result is not yet available.
  • overallResult.status: The final status of the verification, either approved, declined, error, or needs-review.
  • overallResult.concerns: Array of failed checks if the application is not approved.
  • servicesResults: Detailed information about the verification services used and their results.

The response also contains URI:s (links) to all the digital content such as images and videos. These should be downloaded separately if wanted. This is described later in the documentation.

Here is an example of a response, some parts have been shortened to make it easier to read

{
  "id": "645cae276a9ba02a2daa09ff",
  "application": {
    "fields": [],
    "documents": [
      {
        "issuingCountry": "unknown",
        "documentType": "unknown",
        "files": [
          {
            "id": "645cae2c6a9ba02a2daa1021",
            "kind": "front",
            "mediaType": "image/jpeg",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/17f70.....jpeg?t=168....442"
          },
          {
            "id": "645cae2c6a9ba02a2daa1022",
            "kind": "back",
            "mediaType": "image/jpeg",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/fb133......jpeg?t=1683796112029&s=4fb...b247"
          }
        ]
      }
    ],
    "selfie": {
      "files": []
    }
  },
  "processingState": "done",
  "verificationTypes": ["data-extraction"],
  "metadata": {
    "platform": "web",
    "sdkVersion": "v7.1.0",
    "ipAddress": "85.253.24.175",
    "country": "SWE",
    "city": "Stockholm",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36",
    "savedRequestId": "0092b2d23215236b9e1c876efabadb8ab9b67eb8ebc29edf0ed27e1cadc2c3ed",
    "createdAt": "2023-05-11T08:58:15.605Z",
    "serverVersion": "v1.4.0",
    "livenessSchemaVersion": "0.5",
    "locale": "en",
    "flowName": "[FLOW]"
  },
  "additionalFiles": [],
  "overallResult": {
    "status": "approved",
    "comments": [
      {
        "service": "doc-check",
        "status": "approved",
        "comment": "No issues found."
      }
    ],
    "concerns": [],
    "validationDate": "2023-05-11T08:58:19.562Z"
  },
  "servicesResults": {
    "docCheck": {
      "serviceType": "doc-check",
      "verifier": "Doc-checker",
      "comment": "No issues found.",
      "processingState": "done",
      "status": "approved",
      "extracted": {
        "ocr": [
          {
            "category": "Document number",
            "content": "SPECI2021",
            "contentType": "string"
          },
          {
            "category": "Date of expiry",
            "content": "2031-08-02",
            "contentType": "date"
          }
        ],
        "mrz": [
          {
            "category": "Issue country",
            "content": "NLD",
            "contentType": "country",
            "valid": true
          },
          {
            "category": "Document number",
            "content": "SPECI2021",
            "contentType": "string",
            "valid": true
          }
        ],
        "nfc": [],
        "images": [
          {
            "kind": "front",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/d73c......jpeg?t=1683796112030&s=659....b53"
          },
          {
            "kind": "back",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/d2132.....jpeg?t=1683796112030&s=3d33...4ed"
          },
          {
            "kind": "barcode",
            "uri": "https:/[PARTNER].getid.ee/files/proxy/images/3dcb.....jpeg?t=1683796112030&s=a6a02...2c4"
          },
          {
            "kind": "portrait",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/c9d2....jpeg?t=1683796112031&s=70da61...1a8"
          },
          {
            "kind": "signature",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/e000....jpeg?t=1683796112031&s=e2359d...627"
          },
          {
            "kind": "ghost-portrait",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/84ae....jpeg?t=1683796112031&s=25f472...f55"
          },
          {
            "kind": "mrz",
            "uri": "https://[PARTNER].getid.ee/files/proxy/images/13f4....jpeg?t=1683796112031&s=1708b1...9f7"
          }
        ]
      },
      "documentDataChecking": [
        {
          "equal": true,
          "valid": true,
          "category": "Document number",
          "conflicts": [],
          "message": "Value is ok",
          "status": "approved",
          "ocr": "SPECI2021",
          "mrz": "SPECI2021",
          "nfc": "",
          "barcode": ""
        },
        {
          "equal": true,
          "valid": true,
          "category": "Date of expiry",
          "conflicts": [],
          "message": "Value is ok",
          "status": "approved",
          "ocr": "2031-08-02",
          "mrz": "2031-08-02",
          "nfc": "",
          "barcode": ""
        }
      ],
      "dataExtractionConsistency": {
        "group": "dataExtractionConsistency",
        "description": "Consistency and validity of extracted data",
        "considers": [
          {
            "name": "documentValidation",
            "type": "clear",
            "status": "approved",
            "description": "Data extracted from the document is consistent and valid."
          }
        ]
      },
      "documentPhotoQuality": {
        "group": "documentPhotoQuality",
        "description": "Document photo quality",
        "considers": [
          {
            "name": "supportedDocument",
            "type": "clear",
            "status": "approved",
            "description": "Document is supported for check."
          }
        ]
      }
    }
  },
  "responseCode": 200
}

Findings as Concerns

If the verification result in the overallResult.status is not approved, the overallResult.concerns array above will contain the failed checks. This is useful for understanding why the verification was not approved.

{
    "overallResult": {
        "status": "declined",
        "concerns":[
        {
          "id":"DC045"
          "message":"Found 2 issue(s)."
          "service":"doc-check"
          "status":"declined"
          },
        {
          "id":"DC047"
          "message":"Fields from ocr and mrz have conflict: ke?123457, ke1234572"
          "service":"doc-check"
          "status":"declined"
          },
        {
          "id":"DC028"
          "message":"Digital tampering of a document suspected."
          "service":"doc-check"
          "status":"declined"
          }
        ]
    },
}

Download collected documents via the API

The response contains the full URI:s to all the digital content such as images and videos. These can be downloaded separately towards the files endpoint.

Curl example:

curl --location 'https://[yourEnvironment].sb.getid.dev/files/proxy/images/13f4eeb0a573bf6c15cf2954c87a7f6fb7a872d279dbe249f2e1f009c712f8b0.jpeg?t=1683797273107&s=f2988897cb74c858f76c89d81cdb95268adcbdb0de935b3bb145249c1a24c878' \
--header 'x-sdk-key: {SDKKey}' \
--header 'Content-Type: application/json'

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