Client & Server SDKs
Platform-specific SDK guides for collecting age assurance signals across web, iOS, and Android — plus backend integration examples for Node.js and Python.
Want to see it working first? Clone the a3-quickstart repo — a complete React + Express example you can run in under a minute.
All three client SDKs (Web, iOS, Android) produce identical JSON payloads. Your backend integration code works the same regardless of which platform the signals came from.
The fifth category (account longevity) is your own data — add account_age_days
from your database when you call the API.
Browser SDK (@a3api/signals)
The @a3api/signals npm package passively collects 4 of the 5 signal categories
automatically — behavioral metrics, input complexity, device context, and
contextual signals. It outputs the exact payload shape the API expects. Zero
runtime dependencies, <5KB gzipped.
npm install @a3api/signals
Vanilla JavaScript
import { createSignalCollector } from '@a3api/signals';
// Start collecting when the page loads
const collector = createSignalCollector();
// Later (e.g., on form submit, checkout, or after a few seconds of interaction)
const signals = collector.getSignals();
// signals = { behavioral_metrics, input_complexity, device_context, contextual_signals }
// Send to YOUR backend — never expose your API key in the browser
await fetch('/api/assess-age', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signals),
});
collector.destroy();
React
import { useSignalCollector } from '@a3api/signals/react';
function AgeGatedPage() {
const { getSignals, isReady } = useSignalCollector();
async function handleSubmit() {
const signals = getSignals();
if (!signals) return;
await fetch('/api/assess-age', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signals),
});
}
return <button onClick={handleSubmit} disabled={!isReady}>Continue</button>;
}
Vue
<script setup>
import { useSignalCollector } from '@a3api/signals/vue';
const { getSignals, isReady } = useSignalCollector();
async function handleSubmit() {
const signals = getSignals();
if (!signals) return;
await fetch('/api/assess-age', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signals),
});
}
</script>
<template>
<button @click="handleSubmit" :disabled="!isReady">Continue</button>
</template>
The SDK collects signals passively from standard browser events — it does not make any API calls. Your API key stays on your server.
iOS SDK (A3Signals)
The A3Signals Swift package passively collects the same 4 signal categories as
the browser SDK — behavioral metrics, input complexity, device context, and
contextual signals — using native iOS APIs (gesture recognizers, NotificationCenter,
UIAccessibility). Distributed via SPM and CocoaPods, iOS 15+, zero dependencies.
# Swift Package Manager — add to Package.swift or via Xcode:
https://github.com/a3api/a3api-ios
UIKit
import A3Signals
// Start collecting when the view loads
let collector = createSignalCollector(options: SignalCollectorOptions(
rootView: view,
sourceURL: launchURL // from deep link / universal link (optional)
))
// Later (e.g., on form submit or button tap)
let signals = collector.getSignals()
// signals encodes to the same JSON as the browser SDK
// Send to YOUR backend
let data = try JSONEncoder().encode(signals)
URLSession.shared.uploadTask(with: request, from: data).resume()
// When done (e.g., in deinit or viewDidDisappear)
collector.destroy()
SwiftUI
import A3Signals
// Attach at the root of your view hierarchy
NavigationStack {
ContentView()
}
.collectSignals()
// In any descendant view:
struct CheckoutView: View {
@Environment(\.signalCollector) var collector
var body: some View {
Button("Continue") {
let signals = collector?.getSignals()
// send to your backend
}
}
}
Android SDK (io.a3api:signals)
The io.a3api:signals Kotlin library passively collects the same 4 signal
categories using native Android APIs (OnTouchListener, VelocityTracker,
TextWatcher, AutofillManager). Distributed via Maven Central, API 23+. Only
dependency is kotlinx-serialization-json.
// build.gradle.kts
implementation("io.a3api:signals:0.1.0")
View-based (Activity/Fragment)
import io.a3api.signals.createSignalCollector
import io.a3api.signals.SignalCollectorOptions
// Start collecting in onCreate / onViewCreated
val collector = createSignalCollector(SignalCollectorOptions(
rootView = binding.root,
sourceUri = intent.referrer, // from Activity.getReferrer() (optional)
))
// Later (e.g., on button click)
val signals = collector.getSignals()
// signals serializes to the same JSON as the browser SDK
val json = Json.encodeToString(signals)
// Send to YOUR backend via Retrofit, Ktor, OkHttp, etc.
// When done (e.g., in onDestroy)
collector.destroy()
Jetpack Compose
import io.a3api.signals.compose.rememberSignalCollector
@Composable
fun AgeGatedScreen() {
val signalState = rememberSignalCollector()
Button(onClick = {
val signals = signalState.getSignals()
// send to your backend
}) {
Text("Continue")
}
}
// Collector is automatically destroyed when the composable leaves composition.
End-to-End Integration
The full flow has three steps:
- Client — SDK collects signals on the device (browser, iOS, or Android)
- Backend — Your server receives signals, adds
os_signal,user_country_code, and optionallyaccount_longevity, then calls A3 - Response — Your server receives the verdict and acts on it
Backend: Node.js (Express)
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) => {
// req.body = signals from the browser SDK
const result = await a3.assessAge({
os_signal: 'not-available', // browsers have no OS signal
user_country_code: 'US', // from your GeoIP or user profile
...req.body, // behavioral_metrics, input_complexity, device_context, contextual_signals
account_longevity: {
account_age_days: getUserAccountAgeDays(req.user), // from your database
},
});
// Act on the verdict
if (result.verdict === 'OVERRIDE' || result.assessed_age_bracket !== '18-plus') {
// User assessed as minor — apply age-appropriate restrictions
}
// Store the cryptographic receipt in YOUR logs for audit
await saveVerificationToken(req.user.id, result.verification_token);
res.json({ verdict: result.verdict, bracket: result.assessed_age_bracket });
});
Backend: Python (Flask)
import os
from flask import Flask, request, jsonify
from a3api import A3Client, AssessAgeRequest, OsSignal
app = Flask(__name__)
a3 = A3Client(os.environ["A3_API_KEY"])
@app.post("/api/assess-age")
def assess_age():
signals = request.json # from browser SDK
result = a3.assess_age(AssessAgeRequest(
os_signal=OsSignal.NOT_AVAILABLE,
user_country_code="US",
**signals,
account_longevity={
"account_age_days": get_user_account_age_days(request.user),
},
))
save_verification_token(request.user.id, result.verification_token)
return jsonify(verdict=result.verdict, bracket=result.assessed_age_bracket)
Never call the A3 API from the browser. Your API key must stay on your server. The SDK collects signals; your backend proxies them to A3.
Testing your integration? Use @a3api/mock-server
instead of the live API. It validates request shapes identically and returns
deterministic verdicts — no API key, no sandbox quota, no network dependency.
Ideal for CI/CD pipelines and automated test suites.