FaceAssure On-Browser | Getting Started

Integrate seamlessly with our browser-ready SDKs and build privacy-protected web experiences your users will love. Start coding, see results, and unlock the possibilities today.
AgeAssure Fully-Managed On-Browser Release Notes

Latest available version in Production: v1.10.26 & v1.10.27
Endpoints:

  • Production: https://d3ogqhtsivkon3.cloudfront.net/

  • Target browsers: Chrome (mobile and desktop alike)

    This is a documentation for the browser-based experience of Privately’s facial age estimation technology. The UI elements are fully managed by Privately, enabling you to only focus on the communication between your system and Privately’s.

    Prod Version changelogs

    Please refer to Changelog: AgeAssure On-browser fully-managed

    Desktop:

    • Ideal width: 850px

    • Ideal height: 600px

    or

    • If you want to preserve a 16:9 aspect ratio:

      • Minimum required height: 550px

    or

    • If you want to preserve a 4:3 aspect ratio

      • Minimum required height: 600px

    Mobile:
    Minimum width: 375px
    Height: 100%

    Modes of Execution

    Integration mode - uses signed JWT tokens

    • Add 

    ?token=... to activate this mode

    • The end-user will see "Age Check Complete. TX id....". Whether the age check succeeds or not depends on the claims in the request payload.

    • The user will be redirected based on the parameters specified in the JWT token

    • Liveness features are always enabled.

    Demo mode

    • Add the following to activate this mode:

    ?session_id=<your_api_key>&session_password=<your_api_secret>

    • Add stillMode=true to skip active liveness checks. Passive liveness checks are always on by default.

    • Add shi=true to display instructions screen to the user.

    • Ideal for external demonstrations

    Demo mode

    This mode is pretty much plug-and-play. You may supply your session_id and session_password as query string parameters, and test out the system. This option has limited configurability (such as age limits), therefore it might not be suitable for production at large scale.

    Cross-Origin Communication with session id/password pairs

    It’s possible to set up an iFrame/WebView, embed this page, and ingest the execution output with cross-origin communication via postDocument(), with a structure as follows:

    {
        age_identified: "25+",
        gate_identified: 25, // the highest possible age gate from presets (16, 21, 25)
        minAge: 33.5,
        maxAge: 37.2,
        transaction_id: "..." // a unique transaction ID for your records
        status: "AGE_CHECK_COMPLETE", // or the error code if the estimation wasn't done
        score: 1, // the confidence of the interval [minAge, maxAge]
        exp: 1640995200,
        nbf: 1609459200,
        iat: 1609455600
    }

    In order to receive this message, you should add an event listener to your parent page:

    window.addEventListener('message', function(e) {
        // Get the sent data
        const data = e.data;
        console.log(data);
        /* Surround the event message processing against
        unexpected messages from sources other than the on-browser page: */
        try
        {
            var t = JSON.parse(data);
            var age_identified = t["age_identified"],
                gate_identified = t["gate_identified"],
                min_age = t["minAge"],
                max_age = t["maxAge"],
                execution_status = t["status"];
            //alert(t)
            //alert("age_identified = ", age_identified)
            /* Your execution logic goes here */
        }
        catch(err)
        {
            //alert("not processing: ", data) // OR the one below
            alert("I skip a message, because: ", err)
        }
    });

    We nevertheless recommend instead a JWT-based communication when you deploy with the “Integration mode” - see below.

    Integration mode - Using JWT

    In integration mode, we assume that you pass a JWT request token as a base-64 query string parameter called token. The payload of this token, once parsed, should resemble to the following structure:

    {
      "iss": "https://your.url.where.we.find/your/public/key",
      "sub": "9e39aa249a259c1779a5209f4fe1b57b"
      "aud": "https://api.privately.swiss"
      "exp": 1640995200,
      "nbf": 1609459200,
      "iat": 1609455600,
      "jti": "<your-custom-transaction-id>",
      "vid": "<your-custom-verification-id>",
      "ver": "v1.10.26" // (or higher)
      "rdr": "https://your.custom.url/your-custom-route",
      "age": 21,
      "cfd": 0.9,
      "rtf": "interval",
      "rtb": "callback",
      "shi": true
    }

    Standard Claims

    iss, sub, aud, exp, nbf, iat and jti are part of the standard JWT claims.

    • iss is the URL where we can query your public key you used to sign your claims.

    • sub should contain a value generated from your side. It will be helpful in our shared analytics. While Privately can be completely abstracted from the logic with which this value is generated, it will be helpful for you to distinguish transactions for unique clients, deployment environments, etc.

    • jti should indicate a unique transaction ID. Its value should be generated from your side, so that you can avoid replay attacks or fake transaction payloads.

    • exp (expires-at), nbf (not-before) and iat (issued-at) dates indicate the validity of the request payload. Our system will reject requests if the processing time does not comply the constraints of these dates.

    Showing preparation instructions: shi Claim

    shi (default value: false), shall define whether we show the initial instructions screen to the user (see below). When shi is set to true, This screen allows the user to prepare themselves, select a camera for the test, or scan a QR code to continue the process on a mobile device.

    image-20250605-133241.png

    Execution Behavior: age claim

    • age shall contain the target age in years that Privately should use for estimation. If we observe the test subject’s age to be higher than the confidence level identified in the cfd claim, then we will return a positive response. See “Recommended Age Gates”

    Our system delivers liveness checks by default. Be advised that we do not allow configuration of our liveness features. Without these, we do not guarantee the same level of assurance of our age checks. We reserve the right to override this setting based on the customer's use-case. Please contact us if you’d like to have such a bespoke configuration

    Return Format: rtf Claim

    rtf shall define the return format in the rlt claim in our response token once the age check is complete. Options shall be:

    • “query” [DEFAULT OPTION]: Given your agecfd , and liv requirements, the system will return true or false in the rlt response field.

    • “interval”rlt response field shall include sub-fields minAgemaxAgescore and gate. These will let you run your future queries on the output credentials without having the user redo their age checks until the expiry of the response. For adulthood detection, gate will be an integer that gives the highest gate that we can guarantee from our list of preset gates (16, 21, 25). If there are any issues in the execution, we will return these values as 0 (see “Current Behaviors and Error Codes” section).

    Return Behavior: rtb and rdr Claims

     rtb shall define the return behavior once the age check is complete. Options shall be:

    • “redirect” [DEFAULT OPTION]: The user shall be redirected to the URL you’ve defined in rdr , with our response JWT payload as a query string parameter token :  https://your.custom.url/your-custom-route?token=<ourJWTtoken>

    • “message”: The token will be sent via postDocument() to the parent page that embeds our page. The browser page will not navigate anywhere.

      Just like the demo mode, you can still listen to cross-document message events. However, this time the payload will be structured as follows:

      {
          token: "{encodedHeaders}.{encodedPlayload}.{encodedSignature}"
      }
    • “callback”: Repeats the behavior in “message” mode, and also make the full results sent from our servers to the webhook you register in rdr. In other words, we’ll send HTTP POST to: https://your.custom.url/your-custom-route?token=<ourJWTtoken> The browser page shall not navigate anywhere.

    If you use the “callback” option,

    1. We recommend that you to keep a track of valid JWT IDs that you’ve sent within the jti claim. This way you can avoid fake responses with unknown JWT IDs.

    2. Please further indicate if you’d like to use additional authentication mechanisms for the webhook you’ll register.

    3. If you want to register a single webhook for multiple assets (subclients, deployments, endpoints, experiments,…) etc., we recommend you to distinguish these assets via separate values in the sub claim. This will help yourself and us better keep track of the analytics associated with these separate assets.

    Verification identifier: vid claim

    Your business logic may require you to give a fixed number of retries to an end-user. In such cases, you can use the optional vid claim to keep track of the number of attempts.

    Version selection identifier: ver claim

    • The JWT authentication token may now have the mandatory claim "ver", to indicate which version you'd like to use.

    • This is particularly interesting if you would like to incrementally rollout and/or perform A/B tests on the features

    Theme support: thm claim

    • The JWT authentication token may now have the optional claim "thm", to indicate font and coloring options.

    As exemplified below, you can specify font families, font colors, border and background colors within this claim

    "thm": {
          "headerFontFamily": "https://fonts.googleapis.com/css2?family=Paytone+One&display=swap",
          "descFontFamily": "https://fonts.googleapis.com/css2?family=Poppins:wght@400;600&display=swap",
          "infoFontFamily": "https://fonts.googleapis.com/css2?family=Poppins:wght@600&display=swap",
          "cameraSelectorFontFamily": "https://fonts.googleapis.com/css2?family=Paytone+One&display=swap",
          "buttonFontFamily": "https://fonts.googleapis.com/css2?family=Baloo+2:wght@700&display=swap",
          "headerFontColor": "#0EA5A4",
          "descFontColor": "#334155",
          "infoFontColor": "#0F172A",
          "cameraSelectorFontColor": "#10B981",
          "cameraSelectorBgColor": "#E6F7D9",
          "cameraSelectorBorderColor": "#10B981",
          "iconBgColor": "#E6F7D9",
          "iconBorderColor": "#10B981",
          "startButtonBgColor": "#10B981",
          "startButtonTextColor": "#053B2D"
        }

    Timeout limit: tmt claim

    Determines how many seconds do you allow the users to complete the transaction. Default is 60 seconds.

    Sample JWT Request Generation

    In order to generate a “compliant” but unsigned JWT request payload, you use a snippet similar to the one below. In order to run the snippet below, you can paste it into a file called jwtGeneration.js and then run node jwtGeneration.js :

    const crypto = require("crypto");
    function getRandomHexID() {
        return crypto.randomBytes(32).toString("hex")
      }
    function generateClaimsForJWT(args)
    {
      let timestampGenuine = Date.now() / 1000; // the number of milliseconds since January 1, 1970 00:00:00.
      timestampGenuine = ~~timestampGenuine;
      let claims = {
        iss: "client-issuer-url", // the URL where we'll check the public keys
        sub: "9e39aa249a259c1779a5209f4fe1b57b",
        aud: "https://api.privately.swiss", // the URL where we'd supply the publich keys
        exp: timestampGenuine + 100, // Expiration time -- assuming genuinely 100 seconds after issued-at
        nbf: timestampGenuine, // not-before, which is right now
        iat: timestampGenuine - 2, // issued-at -- assuming 2 seconds before
        jti: getRandomHexID().replaceAll('-', ''),
        rdr: "https://privately.eu", // redirection URL
        age: 18,
        cfd: 0.9
      }
      return claims;
    }
    function buildJwt(claims) {
        const header = {
          alg: "HS256",
          typ: "JWT",
        };
        const HMACSHA256 = (stringToSign, secret) => "not_implemented";
        const encodedHeaders = encodeURIComponent(btoa(JSON.stringify(header)));
        const encodedPlayload = encodeURIComponent(btoa(JSON.stringify(claims)));
        const signature = HMACSHA256(`${encodedHeaders}.${encodedPlayload}`, "mysecret");
        const encodedSignature = encodeURIComponent(btoa(signature));
        const myjwt = `${encodedHeaders}.${encodedPlayload}.${encodedSignature}`;
        //console.log(myjwt);
        return myjwt;
      }
    function demo(args)
    {
        let claims = generateClaimsForJWT(args);
        let myJWT = buildJwt(claims);
        console.log(myJWT);
    }
    const args = process.argv.slice(2);
    demo(args);

    We will require you to:

    • Sign your payloads

    • Point to the public key that we can use to verify your JWT request

    • Give the name of the algorithm 'RS256', 'ES256', etc. that you've used for encryption.

    • We strongly recommend you to use off-the-shelf packages such as https://www.npmjs.com/package/jsonwebtoken to sign and verify JWT tokens in production.

    The Response Payload

    The response payload’s claims shall have the following structure: 

    {
      "iss": "https://api.privately.swiss",
      "sub": "9e39aa249a259c1779a5209f4fe1b57b"
      "aud": "https://your.url.where.we.find/your/public/key",
      "exp": 1640995200,
      "nbf": 1609459200,
      "iat": 1609455600,
      "age": <mirrors the age claim from the request payload>,
      "liv": <whether liveness check was enabled or not for this TX>,
      "jti": "<your-custom-transaction-id>",
      "rlt": true | {"minAge":..., "maxAge": ...},
      "rsn": "AGE_CHECK_COMPLETE",
      "ufi": []
    }
    • Our custom field rlt contains information based on the return format claim of the original request.

      • If the requesting JWT’s rtf is set to “query”, rlt shall be a boolean on whether the end-user satisfies the constraints laid out in the request payload’s agecfdlivclaims.

      • If the requesting JWT’s rtf is set to “interval”, rlt shall be of format {"minAge":..., "maxAge": ...}, where[minAge, maxAge]is the 95% confidence interval.

    • We added another custom field rsn , which might give more information if we deem necessary, such as complete_transactionuser_did_not_follow_instructions, etc.

    • Yet another claim, ufi, shall indicate a list of instructions that the user hasn’t completed before the end of the age check. Possible values that can be inserted to this list are: NO_FACE , STAY_STILL , LOOK_STRAIGHTCENTRE_FACETURN_LEFTTURN_RIGHTALIGN_YOUR_FACE_WITH_THE_CAMERA_UP,  ALIGN_YOUR_FACE_WITH_THE_CAMERA_DOWNSLIGHTLY_TILT_YOUR_HEAD_LEFTSLIGHTLY_TILT_YOUR_HEAD_RIGHTOPEN_YOUR_MOUTH, KEEP_YOUR_MOUTH_OPENCLOSE_YOUR_MOUTHSLOWLY_COME_CLOSER_TO_THE_CAMERASLOWLY_DISTANCE_YOURSELF_FROM_THE_CAMERATOO_DARK

    Given a response payload claims, here’s a snippet that verbosely demonstrates what we do to sign our response JWT:

    buildJwt(claims){
        const header = {
        "alg": "RS256",
        "typ": "JWT"
        }
        const RSA256 = (stringToSign, secret) => "library_implementation"
        const encodedHeaders = encodeURIComponent(btoa(JSON.stringify(header)))
        const encodedPlayload = encodeURIComponent(btoa(JSON.stringify(claims)))
        const signature = RSA256(`${encodedHeaders}.${encodedPlayload}`, "mysecret")
        const encodedSignature = encodeURIComponent(btoa(signature))
        const myjwt = `${encodedHeaders}.${encodedPlayload}.${encodedSignature}`;
        return myjwt
    }

    The output, myjwt , would be added as a query string parameter (token) to the redirection URL obtained from the request payload.

    Querying Transaction Results Outside the Execution Time

    You may encounter cases where you will need to query the transaction results outside the execution time, due to network availability and/or in order to reuse an existing age check result. In this case, you may send an HTTP POST request to the following endpoint:

    JWT result query endpoint: https://wycmdhq5c2.execute-api.eu-west-1.amazonaws.com/default/d-privately-age-services

    Sample payload (raw/JSON):

    {
        "request_type": "query_jwt_result",
        "api_key": "<your_api_key>",
        "transaction_id": "<value_set_in_jti_claim>"
    }

    Success case: If such a transaction has already happened, The output of this request will be a base64 string of our signed JWT payload, which follows the same format described above in “The Response Payload”.

    When we issue our JWT response token, we set its exp (“expiration”) claim 10 minutes from its issuance date. Therefore, depending on the time in which you query the results, you may receive a token that has already expired and therefore cannot be validated. However, the raw payload will still be included for your inspections at any time.

    Fail case: If such a transaction doesn’t exist with your api key, you will receive a response with the HTTP status 400:

    {
        "request_not_complete": "<the_invalid_transaction_id>"
    }

    In the context of data privacy laws such as GDPR, Privately is a “Data Processor” - i.e., our software provides our age estimation assessments, but we cannot take decisions on your behalf. As a relying party, it is incumbent on you to implement our solution to the level of effectiveness that you are required to put in place by laws set in the jurisdictions that you operate.

    Below are the age buffers that we recommend you to set in order to ensure that you’ve implemented a highly effective solution

    Age display to the end-user

    • "Age Check Complete." in integration mode (i.e., the prediction won't be revealed to the end-user)

    • "<16", "16-21", "21+" in Demo mode.

    Target age gates

    The system will accept target age gate inputs as, or will convert them to, 16, 21 and 25.

    These age gates are determined by the inputs we have gathered from regulators, the market, our internal benchmarking, and our public EAL3 certifications:

    • Bearing in mind the EU regulations for parental consent, 16 is a good buffer age to determine whether a given user can ask for verified parental consent.

    • 25 comes from the Challenge-25, where businesses are obliged to check for an ID for stronger age verification if the user appears to be below the age of 25.

    • 23 is the buffer age for ensuring highly effective performance for the 18 age gate

    • 21 is the buffer age for ensuring highly effective performance for the 16 age gate

    User Experience with QR Codes

    When a user opts to scan a QR code and continue their tests on another device, the following effects unfold:

    • It is understood that the user has already read the instructions from their first device, thus QR codes lead to URLs that have &shi=false in the query string parameters.

    • Upon the successful completion of the test in the second device, the user will not be able to submit an age check from the first device.

    • If you have chosen to embed our page as an iFrame to your page, you should make sure that you’ve set rtb:callback, and terminate our iFrame as soon as you receive the results from your webhook.

    The QR code is intended to be used just once, therefore it is deleted:

    • As soon as it is scanned

    • As soon as an age check is completed (exceptions: if user denies camera, or their devices are deemed incompatible to start the execution).

    • If more than a minute has passed since its issuance.

    Current Behavior and Error codes

    Incident

    Behavior

    User completes the transaction

    User is redirected to the url defined in rdr, with the result encoded in rlt as true or false. The field rsn will be filled with complete_transaction

    A previously consumed JWT token is presented

    • Alert message: "Age check token has already been used. Please verify with your game publisher"

    • Reload the page without any communication back to client's systems

    JWT token includes sub (user id) that previously did an age check

    • Allow the user to perform an age check

    No token is presented, or the JWT token does not parse with the parseJwt function

    • Alert message: "Age check publisher credentials are invalid. Please verify with your game publisher"

    • Since JWT (and hence redirection URL) might be tampered, no redirection happens. However, the transaction will be logged in Privately servers with the signal INVALID_JWT

    User does not follow the instructions, and thus a timeout occurs

    • Alert message: "Your age test has exceeded the time limit before we could sample a good number of images. Please follow the instructions to align your face and complete the test."

    • Redirect the user back to the URL specified in rdr, with rlt=false, rsn="USER_DID_NOT_FOLLOW_INSTRUCTIONS".

    User closes the window without finishing the check

    • Redirection not possible, but we will log this transaction with the signal "USER_PREMATURELY_CLOSED"

    User denies camera access to our interface

    • Redirection with the the rsn claim = CAMERA_PERMISSIONS_NOT_GRANTED 

    A presentation attack is detected (printed photo / face masks are physically presented to the camera)

    • Redirect the user back to the URL specified in rdr, with rlt=false, rsn="FACE_SWAP_DETECTED"

    A screen attack is detected (image/video shown from another device)

    • Redirect the user back to the URL specified in rdr, with rlt=false, rsn="SCREEN_ATTACK_DETECTED"

    An injection attack is detected (a pre-recorded media fed directly through virtual devices)

    • Redirect the user back to the URL specified in rdr, with rlt=false, rsn="VIRTUAL_CAMERA_DETECTED"

    Redirection URL is not valid

    • If the JWT token is structurally valid, the redirection will happen, even if the link is broken

    • If JWT is invalid, then no redirection will happen.

    Additional Considerations

    • Before large scale deployments please consult us for optimized scaling solutions.

    • For customised frontend, contact us for a specific version of our "white-label" product option, e.g. integrating your visual assets

    • When possible, Privately stores the transaction outcomes also in its databases. Upon request, we can open an endpoint to you so that they can query these records. Additional logs can be made available based on mutually agreed design choices.