Web Integration Guide
End-to-end guide for integrating A3 age assurance into web applications — where no OS age signal exists and PROVISIONAL verdicts are expected.
The Web Gap
Browsers have no equivalent of Apple's Screen Time or Google's Family Link — there
is no OS-provided age signal on the web. Every web request sends
os_signal: "not-available", and A3 returns a PROVISIONAL verdict based
entirely on supplementary signals (behavioral metrics, input complexity, device
context, contextual signals, and account longevity).
This isn't a limitation — it's the expected path. A3's Signal Fusion engine was designed for exactly this scenario. The key difference from native apps is that your supplementary signals are the assessment, not a secondary check. The more signal categories you provide, the higher the confidence score and the stronger your compliance position under AB 1043 §1798.501(b)(2)(B).
Desktop users: The engine fully supports pointer-mode behavioral
signals for desktop/laptop interactions. Set interaction_mode: "pointer" and
use signals like avg_click_precision, mouse_velocity_mean,
mouse_path_straightness, hover_dwell_time_ms, typing_speed_wpm, and
keystroke_interval_variance instead of touch-specific signals. The
@a3api/signals SDK automatically detects the input type and collects the
appropriate signals.
Architecture
The integration follows a three-step flow. Your API key never leaves your server.
- Browser —
@a3api/signalspassively collects 4 of 5 signal categories from standard browser events - Your Server — receives signals, adds
os_signal: "not-available",user_country_code, and optionallyaccount_longevity, then calls A3 - Response — your server receives the verdict and tells the browser what to enforce
Step-by-Step Walkthrough
1. Install dependencies
Browser — install the signal collection SDK:
npm install @a3api/signals
Server — install the A3 backend SDK:
npm install @a3api/node2. Collect signals in the browser
Start the collector when the page loads and retrieve signals when you need the assessment (e.g., on form submit, checkout, or after a few seconds of interaction).
import { createSignalCollector } from '@a3api/signals';
const collector = createSignalCollector();
// Later (e.g., on form submit or after interaction)
const signals = collector.getSignals();
const res = await fetch('/api/assess-age', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signals),
});
const { verdict, bracket, confidence } = await res.json();
// Act on the verdict (see "Verdict Handling" below)
collector.destroy();Never expose your API key in the browser. The SDK collects signals only — it makes no API calls. Your server proxies the signals to A3.
3. Forward signals from your backend
Your backend receives the browser signals, adds the required fields, and calls A3.
import express from 'express';
import { A3Client } from '@a3api/node';
const app = express();
app.use(express.json());
const a3 = new A3Client({ apiKey: process.env.A3_API_KEY! });
app.post('/api/assess-age', async (req, res) => {
const result = await a3.assessAge({
os_signal: 'not-available', // always for web
user_country_code: 'US', // from GeoIP or user profile
...req.body, // signals from the browser SDK
account_longevity: {
account_age_days: getUserAccountAgeDays(req.user),
},
});
// Store the cryptographic receipt for audit
await saveVerificationToken(req.user.id, result.verification_token);
res.json({
verdict: result.verdict,
bracket: result.assessed_age_bracket,
confidence: result.confidence_score,
});
});4. Handle the response
A3 returns a PROVISIONAL verdict with an assessed_age_bracket and
confidence_score. See the Verdict Handling section below
for exactly what to do with each combination.
{
"confidence_score": 0.82,
"verdict": "PROVISIONAL",
"os_signal_age_bracket": "undetermined",
"assessed_age_bracket": "under-13",
"signal_overridden": false,
"internal_evidence_only": true,
"evidence_tags": [
"os_signal_not_available",
"low_touch_precision",
"rapid_scroll_velocity",
"rapid_form_completion",
"high_autocorrect_rate",
"low_word_complexity",
"new_account",
"rule_source:1798.501.b.3.B"
],
"verification_token": "eyJ2IjoxLC..."
}
Store the verification_token in your logs — even for PROVISIONAL
verdicts. This is your cryptographic proof of what A3 assessed. A3 retains
zero user data.
Understanding PROVISIONAL Responses
When os_signal is "not-available", the verdict is always PROVISIONAL.
This means A3 assessed the user's age bracket based entirely on supplementary
signals — there was no OS signal to corroborate or contradict.
What the confidence score means
The confidence_score (0–1) reflects how certain the engine is about the
assessed_age_bracket. For web integrations, two factors determine this score:
-
Signal strength — how clearly the signals point to a specific age bracket. A user with very low touch precision, rapid scrolling, high autocorrect reliance, and a brand-new account produces a high confidence score for
under-13. -
Coverage penalty — the engine reduces maximum achievable confidence when fewer than all five signal categories are present:
| Categories Provided | Max Confidence Factor | Example |
|---|---|---|
| 1 | 88.0% | Behavioral metrics only |
| 2 | 91.0% | Behavioral + input complexity |
| 3 | 94.0% | + device context |
| 4 | 97.0% | + contextual signals |
| 5 | 100.0% | All categories (no penalty) |
This means a web integration sending only behavioral metrics can reach at most 88% of the raw confidence score. With all five categories, the full score applies.
Bracket mapping
The assessed_age_bracket returned for PROVISIONAL verdicts uses the same
brackets as native: under-13, 13-15, 16-17, 18-plus, or undetermined
(when signals are too sparse to classify). Your enforcement logic should map
these brackets to age-appropriate restrictions for your product.
Verdict Handling
This is the critical piece: what should your application do for each verdict + bracket + confidence combination? The table below provides recommended enforcement actions for web integrations.
Action table
| Verdict | Bracket | Confidence | Recommended Action |
|---|---|---|---|
PROVISIONAL | under-13 | ≥ 0.78 | Block or hard-restrict. Strong evidence of a child. Disable mature content, age-gate purchases, restrict social features. Log the verification_token. |
PROVISIONAL | under-13 | < 0.78 | Soft-restrict. Evidence suggests a child but below the clear & convincing threshold. Show an age gate or parental-consent flow. Consider requesting face estimation as a fallback. |
PROVISIONAL | 13-15 | ≥ 0.78 | Apply teen restrictions. Limit content per your platform policy (e.g., no mature content, limited ad targeting, restricted DMs). |
PROVISIONAL | 13-15 | < 0.78 | Light restrictions + age gate. Present a soft age verification prompt. Restrict the most sensitive features while allowing general access. |
PROVISIONAL | 16-17 | ≥ 0.78 | Near-adult access with guardrails. Allow most content but restrict age-gated actions (alcohol, gambling, tobacco). |
PROVISIONAL | 16-17 | < 0.78 | Standard access. Evidence leans toward 16-17 but isn't conclusive. Serve normally with optional age-gate for regulated content. |
PROVISIONAL | 18-plus | ≥ 0.78 | Full access. Strong adult signals. Serve all content normally. |
PROVISIONAL | 18-plus | < 0.78 | Full access, flag for monitoring. Likely adult but signals are ambiguous. Serve normally; optionally re-assess on next session with fresh signals. |
PROVISIONAL | undetermined | any | Show age gate. Insufficient signals to classify. Present a standard age-verification prompt or request additional signals. |
The 0.78 threshold aligns with AB 1043's "clear and convincing" evidence
standard — the same threshold the engine uses internally for OVERRIDE
verdicts on native apps. For web PROVISIONAL verdicts, this threshold isn't
enforced by A3 (no OS signal to override), but it serves as a reliable
decision boundary for your enforcement logic.
Example enforcement logic
type Action = 'block' | 'restrict-child' | 'restrict-teen' | 'age-gate' | 'allow';
function getAction(verdict: string, bracket: string, confidence: number): Action {
if (verdict !== 'PROVISIONAL') {
// Native app path — see the assess-age docs for OVERRIDE/REVIEW handling
return bracket === '18-plus' ? 'allow' : 'restrict-child';
}
if (bracket === 'undetermined') return 'age-gate';
if (bracket === 'under-13') {
return confidence >= 0.78 ? 'block' : 'age-gate';
}
if (bracket === '13-15') {
return confidence >= 0.78 ? 'restrict-teen' : 'age-gate';
}
// 16-17 or 18-plus
return 'allow';
}Maximizing Confidence
Web integrations start without an OS signal, so every supplementary signal category you provide directly increases assessment quality. Here's how to get the most out of A3.
Signal category impact
Not all categories contribute equally. Prioritize by weight:
| Priority | Category | Weight | Why It Matters |
|---|---|---|---|
| 1 | Behavioral Metrics | 43% | Touch precision and scroll velocity (mobile) or click precision, mouse velocity, and path straightness (desktop) are the strongest age differentiators. The SDK auto-detects the input type and collects the appropriate signals. |
| 2 | Input Complexity | 28% | Autocorrect reliance and word complexity vary dramatically between children and adults. Collected automatically by the SDK. |
| 3 | Device Context | 10% | OS version and accessibility settings provide contextual normalization. Collected automatically by the SDK. |
| 4 | Contextual Signals | 10% | IP type and timezone offset add environmental context. Collected automatically by the SDK. |
| 5 | Account Longevity | 9% | The only category you provide — account_age_days from your database. A mature account is a strong adult corroborator. |
The @a3api/signals browser SDK automatically collects categories 1–4. You only
need to add account_longevity from your own database.
Practical tips
-
Let the SDK run for a few seconds. Signal quality improves with more interaction data. Trigger
getSignals()after a form submit, a scroll through content, or 3–5 seconds of page interaction — not immediately on load. -
Always send account longevity. It's the easiest signal to add (one database query) and eliminates the 5-category coverage penalty. An account older than 5 years (
account_age_days >= 1825) is a strong adult corroborator. -
Provide all five categories to avoid the coverage penalty. With all five present, the engine operates at 100% confidence factor. Missing even one category caps your maximum confidence at 97%.
-
Re-assess on significant events. Collect fresh signals on checkout, account settings changes, or content access attempts — not just at login. Behavioral patterns may change if a child picks up a parent's device mid-session.
-
Use face estimation as a fallback. For borderline cases (confidence near 0.78), the
face_estimation_resultfield inbehavioral_metricslets you integrate a privacy-preserving selfie scan from Yoti, Privado, or FaceTec. This is optional but can push ambiguous assessments above the threshold.
Next Steps
- Quickstart project — clone-and-run React + Express example you can have running in under a minute
- POST /assess-age — full API reference with request/response schemas
- Client & Server SDKs — platform-specific SDK guides with framework examples
- Signal Collection Overview — deep dive into each signal category
- Testing & CI/CD — mock server for automated tests without burning quota
- AB 1043 Compliance — regulatory context and how A3 maps to the statute