Notifications

Each session has a configured 'time to live' (TTL) which must be above 60 seconds (1 minute). When a session is created, it remains active until its time to live is reached.

To receive the result of an age verification, an HTTP endpoint (notification URL) needs to be listening for a JSON payload in the format below through a POST method.

Notifications require HTTPS and will be reattempted if a 200 status code is not received. It is important to acknowledge the notification with a 200 status code to avoid receiving repeat notifications.

Notifications will be sent multiple times for cases where retries or the resume flow are enabled. It may be possible for a user to initially 'fail' a check type, but subsequently pass on a retry.

Notifications will be sent out for each age verification attempt that a user undergoes.

Below is an example payload:

{
"method": "AGE_ESTIMATION",
"result": true,
"age": 2,
"session_key": "479f57aa-9c98-407d-8854-49768c143564",
"reference_id": "245363",
"id": "479f57aa-9c98-407d-8854-49768c143564",
"timestamp": 1787752834,
"notification_url": "https://mynotification.example.com",
"evidence_id": "479f57aa-9c98-407d-8854-49768c143564",
"state": "COMPLETE",
"check_type": "SESSION",
"sequence_number": 1,
"signature": "SIGNATURE"
}

Within the notification object, Yoti returns a signature, which allows you to verify that the notification originated from Yoti's Age Verification service.

Verifying the signature may be achieved by:

  1. Create a JSON string from the notification payload, stripping out sequence_number and signature. All spaces in this string must be trimmed.

  2. Convert the resulting JSON string into bytes by encoding it as UTF-8.

  3. Hash these bytes using SHA256 (C# and Go only).

  4. Decode the base64-encoded signature to determine its length in bytes and calculate the salt length.

  5. RSA-SHA256 verify the byte value of the JSON string from Step 2 (or hash of the bytes from Step 3) against the byte value of the signature in the webhook response, using PSS padding with SHA256 and the calculated salt length.

The public key is at the bottom of this page.

Example Code

const crypto = require("crypto");
const fs = require('fs');

// Load the public key from a local file
const publicKeyPath = 'keys/public-key.pem';
const publicKey = fs.readFileSync(publicKeyPath, 'utf-8');

function verify(publicKey, signature, payload) {
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(payload);

return verifier.verify(
{ key: publicKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING },
Buffer.from(signature, 'base64')
);
}

// Notification JSON
const data = { /* your data here */ };

const { sequence_number, signature, ...payload } = data;
const verified = verify(publicKey, signature, JSON.stringify(payload).replace(/\s/g, ''));

console.log('Signature Verified: ', verified);
package com.example;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class SignatureVerification {
static {
Security.addProvider(new BouncyCastleProvider());
}

public static void main(String[] args) {
try {
// Load the public key from a local file
String publicKeyPath = "public-key.pem";
PublicKey publicKey = getPublicKey(publicKeyPath);
<?php

// Include the Composer autoload file
require __DIR__ . '/vendor/autoload.php';

// Use the Phpseclib3 library
use phpseclib3\Crypt\RSA;

// Load the public key from a local file
$publicKeyPath = 'keys/public-key.pem';
$publicKey = RSA::loadPublicKey(file_get_contents($publicKeyPath));

// Notification JSON String
$jsonData = '{ ... }';

// Decode the JSON data
$data = json_decode($jsonData, true);

// Remove 'sequence_number' and 'signature' from the data
unset($data['sequence_number']);
$signature = $data['signature'];
unset($data['signature']);

// Convert the modified JSON data back to a JSON string and remove spaces
$jsonString = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
import base64
import json

# Load the public key from a local file
public_key_path = 'keys/public-key.pem'
with open(public_key_path, 'rb') as key_file:
public_key = load_pem_public_key(key_file.read())

def verify(public_key, signature, payload):
try:
public_key.verify(
base64.b64decode(signature),
payload.encode('utf-8'),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
using System;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Signers;

public class RsaPssVerifier
{
public static bool VerifySignature(string publicKeyPem, string webhookResponse)
{
try
{
JsonNode jsonNode = JsonNode.Parse(webhookResponse);
string signature = jsonNode["signature"].GetValue<string>();
jsonNode.AsObject().Remove("signature");
jsonNode.AsObject().Remove("sequence_number");
string messageToVerify = jsonNode.ToJsonString(new JsonSerializerOptions { WriteIndented = false });
package main

import (…)

// Age verification service public key
const publicKey = ``

// Full Webhook response
const message = ``

type Message struct {
Method string `json:"method"`
Result bool `json:"result"`
Age int `json:"age"`
SessionKey string `json:"session_key"`
ReferenceID string `json:"reference_id"`
ID string `json:"id"`
Timestamp int64 `json:"timestamp"`
NotificationURL string `json:"notification_url"`
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwVQ840btpD7bmLW0+tS5
yZOCdhaRV7/uJ4X6BhgnaArwcvdMS3VjjcDICo2e2OZDySpsb2b97DGjYTpWNEAQ
CtU9/Ye0mXlsmfYX8lmzPm5P98EEkqyOF+RWmykbhA8hMrgI9ykqsSoAbCCAhPh1
0J9ugLJVIokEv1o3bnRk3PboUQBHKBunWb1Da7U7X/Feb5RyHG3ksVOKfBOu3AKS
/GobJg/95oo/d79X+7P+e1Z3fdmkmdxMqr/tTs5cj2sKUZPwERpTJYyMCLXMsswn
8ms4jawVcRKyxP1GfPqqc13edwVH49Ial8pjXEpB50G2Wk9yvODvB2cEGeeEJcFI
oQIDAQAB
-----END PUBLIC KEY-----