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, eitherdone
orprocessing
. If the state isprocessing
, the final result is not yet available.overallResult.status
: The final status of the verification, eitherapproved
,declined
,error
, orneeds-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
}
});
Updated 17 days ago