Integration guide

AI Tools

This guide outlines how to configure and retrieve results in the Yoti Age Verification Sandbox. You can simulate user activity via the Sandbox interface or bypass the UI by using API calls to mock responses.

The implementation steps are as follows:

  1. Generate the access token for authorization (bearer)

  2. Create an age verification sandbox session

  3. Mock the Yoti response

    1. Launch the Yoti client-side interface for the user (alternative)

  4. Subscribe to Webhook notifications (optional)

  5. Retrieve results from the Yoti API

Authorization

All API calls will be made using an OAuth Access Token as a bearer token.

The required steps to generate this Access Token and make API calls to Yoti are as follows:

  1. Generate or Register a Private Key Pair

  2. Generate a JWT token using a Yoti private key (.pem file)

  3. Acquire an OAuth Access Token using this JWT Token

  4. Send an API request to Yoti

A token can be reused for multiple requests. There is a limit of 200 active tokens per service/SDK ID. We advise that you use one key for all calls within a scope and obtain a new token every 30 minutes.

You must NOT use a new token for every request.

Getting a Private Key Pair

You will need to create a Yoti sandbox service and generate a private key pair. This will provide you with a .pem file and an SDK ID, which will be used in the subsequent steps.

Generating a JWT Token

We use the private_key_JWT client authentication method from OIDC Core. The JWT will be signed by the RSA private key of the service/SDK ID that you are authenticating as.

Header

Value

Description


alg

PS384

Algorithm. We require the algorithm to be PS384. No other algorithms are accepted.

yes

typ

JWT

Type. Must be the string value JWT.

yes

The following claims must be present in the payload:

Claim

Value

Description

Mandatory?

iss

sdk: <YOUR_SDK_ID>

Issuer. Must be set to the string “sdk:" || SDK ID, e.g. sdk:67d60fe2-5576-49ae-9ac9-ad76b232c5e1.

Yes

sub

sdk: <YOUR_SDK_ID>

Subject. Must be set to the same value as iss

Yes

aud

https://api.example.com

Audience. This must be the full URL for the OAuth client credentials grant endpoint.

Yes

jti

UUID string

JWT ID. We require a valid UTF-8 string of at least 16 bytes and at most 128 bytes (not characters) in length. Each JWT that is issued must use a different jti value; the authorization server will remember iss || jti

Yes

exp

1751700000

Expiry time. The authorization server will refuse to grant client credentials if a request is processed after this time. The expiry time can be a maximum of 30 minutes in the future.

Yes

iat

1751700000

OPTIONAL. Issued at. The authorization server will refuse to grant client credentials if this value is unreasonably far in the past. We have a threshold of 30 minutes

No

nbf

1751700000

OPTIONAL. Not before. The authorization server will refuse to grant client credentials if the request arrives before the “not before” time.

No

Example

const fs = require("fs"); const jwt = require("jsonwebtoken"); const uuid = require("uuid"); const pem = fs.readFileSync("PATH_TO_PEM.pem"); const sdkId = "YOUR_SDK_ID"; const authURL = "https://auth.api.yoti.com/v1/oauth/token"; const issuedAt = Math.floor(Date.now() / 1000); const expiry = issuedAt + 30 * 60; function buildJWT() { const claims = { iss: "sdk:" + sdkId, sub: "sdk:" + sdkId, aud: authURL, jti: uuid.v4(), exp: expiry, iat: issuedAt, }; const headers = { alg: "PS384", typ: "JWT", }; try { const token = jwt.sign(claims, pem, { algorithm: "PS384", header: headers, }); return token; } catch (error) { console.error("Error building JWT:", error); } } const token = buildJWT(); console.log("Generated JWT successfully"); console.log(token);
using System; using System.IO; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.IdentityModel.Tokens; using System.Security.Cryptography; using System.Text; class Program { static void Main() { string pemPath = "PATH_TO_PEM.pem"; string sdkId = "YOUR_SDK_ID"; string authURL = "https://auth.api.yoti.com/v1/oauth/token"; string token = BuildJWT(pemPath, sdkId, authURL); Console.WriteLine("Generated JWT: " + token); } static string BuildJWT(string pemPath, string sdkId, string authURL) { // Read PEM private key string pem = File.ReadAllText(pemPath); var rsa = RSA.Create(); rsa.ImportFromPem(pem.ToCharArray()); var securityKey = new RsaSecurityKey(rsa); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.RsaSsaPssSha384); var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var claims = new[] { new Claim(JwtRegisteredClaimNames.Iss, "sdk:" + sdkId), new Claim(JwtRegisteredClaimNames.Sub, "sdk:" + sdkId), new Claim(JwtRegisteredClaimNames.Aud, authURL), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Exp, (now + 30 * 60).ToString()), new Claim(JwtRegisteredClaimNames.Iat, now.ToString()) }; var tokenDescriptor = new SecurityTokenDescriptor { Issuer = "sdk:" + sdkId, Subject = new ClaimsIdentity(claims), Audience = authURL, Expires = DateTimeOffset.FromUnixTimeSeconds(now + 30 * 60).UtcDateTime, SigningCredentials = credentials }; var handler = new JwtSecurityTokenHandler(); var securityToken = handler.CreateJwtSecurityToken(tokenDescriptor); return handler.WriteToken(securityToken); } }
package main import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "log" "os" "time" "github.com/google/uuid" ) func main() { sdkId := "YOUR_SDK_ID" pemPath := "PATH_TO_PEM.pem" authURL := "https://auth.api.yoti.com/v1/oauth/token" token, err := buildJWT(pemPath, sdkId, authURL) if err != nil { log.Fatal("Error building JWT:", err) } fmt.Println("Generated JWT successfully") fmt.Println(token) } func buildJWT(pemPath, sdkId, authURL string) (string, error) { pemData, err := os.ReadFile(pemPath) if err != nil { return "", fmt.Errorf("failed to read PEM file: %w", err) } block, _ := pem.Decode(pemData) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { // Try PKCS8 if PKCS1 fails parsed, err2 := x509.ParsePKCS8PrivateKey(block.Bytes) if err2 != nil { return "", fmt.Errorf("failed to parse private key: %w", err) } key = parsed.(*rsa.PrivateKey) } now := time.Now().Unix() header, _ := json.Marshal(map[string]string{ "alg": "PS384", "typ": "JWT", }) claims, _ := json.Marshal(map[string]interface{}{ "iss": "sdk:" + sdkId, "sub": "sdk:" + sdkId, "aud": authURL, "jti": uuid.New().String(), "iat": now, "exp": now + 300, }) signingInput := b64url(header) + "." + b64url(claims) hash := crypto.SHA384.New() hash.Write([]byte(signingInput)) signature, err := rsa.SignPSS(rand.Reader, key, crypto.SHA384, hash.Sum(nil), &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthEqualsHash, }) if err != nil { return "", fmt.Errorf("failed to sign JWT: %w", err) } return signingInput + "." + b64url(signature), nil } func b64url(data []byte) string { return base64.RawURLEncoding.EncodeToString(data) }
<?php require_once __DIR__ . '/vendor/autoload.php'; $pemPath = 'PATH_TO_PEM.pem'; $sdkId = 'YOUR_SDK_ID'; $authURL = 'https://auth.api.yoti.com/v1/oauth/token'; $token = buildJWT($pemPath, $sdkId, $authURL); echo "Generated JWT: " . $token . "\n"; function buildJWT(string $pemPath, string $sdkId, string $authURL): string { $pem = file_get_contents($pemPath); $now = time(); $header = json_encode(['alg' => 'PS384', 'typ' => 'JWT']); $claims = json_encode([ 'iss' => 'sdk:' . $sdkId, 'sub' => 'sdk:' . $sdkId, 'aud' => $authURL, 'jti' => bin2hex(random_bytes(16)), 'iat' => $now, 'exp' => $now + 300, ]); $signingInput = base64UrlEncode($header) . '.' . base64UrlEncode($claims); $rsaKey = \phpseclib3\Crypt\PublicKeyLoader::load($pem); $rsaKey = $rsaKey ->withPadding(\phpseclib3\Crypt\RSA::SIGNATURE_PSS) ->withHash('sha384') ->withMGFHash('sha384'); $signature = $rsaKey->sign($signingInput); return $signingInput . '.' . base64UrlEncode($signature); } function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); }
import json import time import os import base64 from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding sdk_id = "YOUR_SDK_ID" pem_path = "PATH_TO_PEM.pem" auth_url = "https://auth.api.yoti.com/v1/oauth/token" def b64url(data: bytes) -> str: return base64.urlsafe_b64encode(data).rstrip(b"=").decode() def build_jwt(pem_path: str, sdk_id: str, auth_url: str) -> str: with open(pem_path, "rb") as f: private_key = serialization.load_pem_private_key(f.read(), password=None) now = int(time.time()) header = json.dumps({"alg": "PS384", "typ": "JWT"}, separators=(",", ":")) claims = json.dumps({ "iss": f"sdk:{sdk_id}", "sub": f"sdk:{sdk_id}", "aud": auth_url, "jti": os.urandom(16).hex(), "iat": now, "exp": now + 300, }, separators=(",", ":")) signing_input = f"{b64url(header.encode())}.{b64url(claims.encode())}" signature = private_key.sign( signing_input.encode(), padding.PSS( mgf=padding.MGF1(hashes.SHA384()), salt_length=padding.PSS.MAX_LENGTH, ), hashes.SHA384(), ) return f"{signing_input}.{b64url(signature)}" token = build_jwt(pem_path, sdk_id, auth_url) print("Generated JWT:", token)
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import java.io.FileReader; import java.security.PrivateKey; import java.time.Instant; import java.util.Date; import java.util.UUID; public class JwtBuilder { private static final String SDK_ID = "YOUR_SDK_ID"; private static final String PEM_PATH = "PATH_TO_PEM.pem"; private static final String AUTH_URL = "https://auth.api.yoti.com/v1/oauth/token"; public static void main(String[] args) throws Exception { PrivateKey privateKey = loadPrivateKey(PEM_PATH); Instant now = Instant.now(); Instant expiry = now.plusSeconds(30 * 60); String token = Jwts.builder() .setIssuer("sdk:" + SDK_ID) .setSubject("sdk:" + SDK_ID) .setAudience(AUTH_URL) .setId(UUID.randomUUID().toString()) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(expiry)) .signWith(privateKey, SignatureAlgorithm.PS384) .compact(); System.out.println("Generated JWT successfully"); System.out.println(token); } private static PrivateKey loadPrivateKey(String pemPath) throws Exception { try (PEMParser parser = new PEMParser(new FileReader(pemPath))) { Object parsed = parser.readObject(); JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); if (parsed instanceof org.bouncycastle.openssl.PEMKeyPair keyPair) { return converter.getPrivateKey(keyPair.getPrivateKeyInfo()); } else if (parsed instanceof PrivateKeyInfo privateKeyInfo) { return converter.getPrivateKey(privateKeyInfo); } throw new IllegalArgumentException("Unsupported PEM format"); } } }

Response

eyJhbGciOiJQUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzZGs6OWVlM2RiNmUtM2UwYy00ZTg1LWIyYTMtOTliZTMxNmY5MDJhIiwic3ViIjoic2RrOjllZTNkYjZlLTNlMGMtNGU4NS1iMmEzLTk5YmUzMTZmOTAyYSIsImF1ZCI6Imh0dHBzOi8vYXV0aC5hcGkueW90aS5jb20vdjEvb2F1dGhvdG9rZW4iLCJqdGkiOiI2N2RlMjM0OC0xOWYyLTRmODctOWE4YS01YTEzZjM5ZWIyNTEiLCJleHAiOjE3NzA3Mjc3MjAsImlhdCI6MTc3MDcyNTkyMH0.lgCJJ57OeV_mJGw91DR71ZRHgOWLbzSK1PYhqC59333PB4fBzxcz4chWHo5Go7vL_vGNYpJQJI-UuuBbZNNCF5OuDnWSQNHIcB_QW7OsYa6al8PcbC21oZrbdBSTbJdk_7S7ARo5noXKf4tH_cnbUkL7I50lMPnJngZBfpLb6ATAjbSWhsKhrT--GlWKs-NB58z8V-PDmG6c93PF7krA6oB964OtIMEHtFiXfbQB61xiYOUyR94G-pLUbftDnvXhyxmT_MZXaZRSIfIy8l75scBoHv96ylSH1yqBcw5vdQoye-Cd6YXGlJLS5QM9fqFm9JYIeOh7DP7Vo4PRTIFlcQ

OAuth Token Grant

Once you have generated a JWT token, this can be used to grant an OAuth Access Token to be used for Yoti API calls.

You must request this from the Yoti authorization server. The request method is POST, and the body from the client must be encoded with the application/x-www-form-urlencoded content type.

POST https://auth.api.yoti.com/v1/oauth/token

The request must include the following form values:

Header

Value

Description

grant_type

client_credentials

OAuth grant type

scope

avs:sessions:create

A space-separated list of one or more scopes that the token will grant access for.

client_assertion_type

urn:ietf:params:oauth:client-assertion-type:jwt-bearer

The OAuth client assertion type

client_assertion

JWT value

The value of the JWT token

comment

“production_key”

Must be valid UTF-8, limited to 128 characters, and comprise only Unicode printable characters.

Examples

const axios = require("axios"); const token = "YOUR_JWT_TOKEN" async function requestOAuthToken() { const formData = new URLSearchParams({ grant_type: "client_credentials", scope: "avs:sessions:create", client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", client_assertion: token, }); try { axios .post("https://auth.api.yoti.com/v1/oauth/token", formData, { headers: { "Content-Type": "application/x-www-form-urlencoded", }, }) .then((response) => { console.log("OAuth token response:", response.data); }); } catch (error) { console.error("Error requesting OAuth token:", error); } }
using System; using System.Net.Http; using System.Threading.Tasks; class Program { static async Task Main() { string token = "YOUR_JWT_TOKEN" var formData = new FormUrlEncodedContent(new[] { new KeyValuePair<string, string>("grant_type", "client_credentials"), new KeyValuePair<string, string>("scope", "avs:sessions:create"), new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), new KeyValuePair<string, string>("client_assertion", token) }); using var httpClient = new HttpClient(); try { var response = await httpClient.PostAsync("https://auth.api.yoti.com/v1/oauth/token", formData); var responseBody = await response.Content.ReadAsStringAsync(); Console.WriteLine("OAuth token response: " + responseBody); } catch (Exception ex) { Console.Error.WriteLine("Error requesting OAuth token: " + ex); } } }
package main import ( "fmt" "io" "log" "net/http" "net/url" ) func main() { token := "YOUR_JWT_TOKEN" formData := url.Values{ "grant_type": {"client_credentials"}, "scope": {"avs:sessions:create"}, "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, "client_assertion": {token}, } resp, err := http.PostForm("https://auth.api.yoti.com/v1/oauth/token", formData) if err != nil { log.Fatal("Error requesting OAuth token:", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println("OAuth token response:", string(body)) }
<?php $token = 'YOUR_JWT_TOKEN'; $formData = http_build_query([ 'grant_type' => 'client_credentials', 'scope' => 'avs:sessions:create', 'client_assertion_type' => 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 'client_assertion' => $token, ]); $context = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/x-www-form-urlencoded', 'content' => $formData, ], ]); $response = file_get_contents('https://auth.api.yoti.com/v1/oauth/token', false, $context);
import urllib.request import urllib.parse token = "YOUR_JWT_TOKEN" form_data = urllib.parse.urlencode({ "grant_type": "client_credentials", "scope": "avs:sessions:create", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": token, }).encode() req = urllib.request.Request( "https://auth.api.yoti.com/v1/oauth/token", data=form_data, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) response = urllib.request.urlopen(req).read().decode() print("Response:", response)
import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class OAuthToken { private static final String TOKEN = "YOUR_JWT_TOKEN"; private static final String AUTH_URL = "https://auth.api.yoti.com/v1/oauth/token"; public static void main(String[] args) { String formData = String.join("&", "grant_type=" + URLEncoder.encode("client_credentials", StandardCharsets.UTF_8), "scope=" + URLEncoder.encode("avs:sessions:create", StandardCharsets.UTF_8), "client_assertion_type=" + URLEncoder.encode("urn:ietf:params:oauth:client-assertion-type:jwt-bearer", StandardCharsets.UTF_8), "client_assertion=" + URLEncoder.encode(TOKEN, StandardCharsets.UTF_8) ); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(AUTH_URL)) .header("Content-Type", "application/x-www-form-urlencoded") .POST(HttpRequest.BodyPublishers.ofString(formData)) .build(); try { HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println("OAuth token response: " + response.body()); } catch (Exception e) { System.err.println("Error requesting OAuth token: " + e.getMessage()); } } }

Response

{ access_token: 'yta_NZuDqGkNOFp4DgwYbdR4TY_X15IEZsuWArACxprJJwp93MCG', token_type: 'bearer', expires_in: 2700, scope: 'avs:sessions:create' }

The access token will be used as the bearer token to authenticate your API requests.

Errors

Due to the OAuth RFC, we will only return 400 or 403 error codes. Details of the error will be found in the response.

Error Code

Details

400

Bad Request

403

Forbidden