_checkContent(String text) async {
final result = await tuteliq.detectUnsafe(
text: text,
ageGroup: AgeGroup.thirteenToFifteen,
);
setState(() => _isSafe = result.safe);
}
@override
Widget build(BuildContext context) {
return Text(_isSafe == true ? 'Content is safe' : 'Checking...');
}
}
```
## Fraud detection and safety extended
These methods cover financial exploitation, romance scams, and coercive behaviour targeting minors. Other endpoints — `detectAppFraud`, `detectMuleRecruitment`, `detectGamblingHarm`, `detectCoerciveControl`, and `detectRadicalisation` — follow the same call pattern shown here.
### Detect social engineering
Identify manipulation tactics designed to trick a child into disclosing information or taking unsafe actions.
```dart theme={"dark"}
final result = await tuteliq.detectSocialEngineering(
text: "If you really trusted me you'd send me your home address. All my real friends do.",
ageGroup: AgeGroup.tenToTwelve,
);
print(result.detected); // true
print(result.tactics); // [Tactic.trustExploitation, Tactic.peerPressure]
print(result.riskScore); // 0.88
```
### Detect romance scam
Analyze conversation text for romantic manipulation patterns that may indicate an adult posing as a peer.
```dart theme={"dark"}
final result = await tuteliq.detectRomanceScam(
messages: [
Message(role: Role.stranger, text: "I've never felt this way about anyone before. You're so mature for your age."),
Message(role: Role.child, text: "Really? That makes me really happy."),
Message(role: Role.stranger, text: "I need you to keep us a secret. People wouldn't understand."),
],
ageGroup: AgeGroup.thirteenToFifteen,
);
print(result.detected); // true
print(result.riskScore); // 0.91
print(result.indicators); // [Indicator.loveBombing, Indicator.secrecyRequest, Indicator.ageFlattery]
```
### Detect vulnerability exploitation
Detect attempts to identify and target emotional or situational vulnerabilities in a child.
```dart theme={"dark"}
final result = await tuteliq.detectVulnerabilityExploitation(
text: "I know you said your parents don't listen to you. I'm different — I actually care. You can tell me anything.",
ageGroup: AgeGroup.thirteenToFifteen,
);
print(result.detected); // true
print(result.riskScore); // 0.85
print(result.vulnerabilities); // [Vulnerability.parentalConflict, Vulnerability.emotionalNeglect]
```
### Analyse multiple texts in one request
Run any supported detection across multiple texts in a single API call to reduce round-trips.
```dart theme={"dark"}
final result = await tuteliq.analyseMulti(
inputs: [
AnalyseMultiInput(text: "You're so special. Nobody else understands you like I do.", ageGroup: AgeGroup.thirteenToFifteen),
AnalyseMultiInput(text: "Can you keep a secret from your mum?", ageGroup: AgeGroup.tenToTwelve),
],
detections: [Detection.socialEngineering, Detection.romanceScam, Detection.grooming],
);
print(result.results[0].detections); // AnalyseMultiDetections(socialEngineering: ..., ...)
print(result.results[1].detections); // AnalyseMultiDetections(grooming: ..., ...)
```
`analyseMulti` is billed per individual input × detection combination, not per request.
## Error handling
The SDK throws typed exceptions that you can catch and inspect.
```dart theme={"dark"}
import 'package:tuteliq/tuteliq.dart';
try {
final result = await tuteliq.detectUnsafe(
text: 'some content',
ageGroup: AgeGroup.tenToTwelve,
);
} on TuteliqError catch (e) {
print(e.code); // e.g. 'AUTH_INVALID_KEY'
print(e.message); // human-readable description
print(e.status); // HTTP status code
}
```
## Configuration options
```dart theme={"dark"}
final tuteliq = Tuteliq(
apiKey: const String.fromEnvironment('TUTELIQ_API_KEY'),
baseUrl: 'https://api.tuteliq.ai', // default
timeout: Duration(seconds: 30), // request timeout
retries: 2, // automatic retries on failure
);
```
## Next steps
Explore the full API specification.
See the Kotlin SDK guide.
# Kotlin SDK
Source: https://docs.tuteliq.ai/sdks/kotlin
Install and use the Tuteliq Kotlin SDK
The Tuteliq Kotlin SDK provides a coroutine-based client for the Tuteliq child safety API. It supports Android 7.0+ (API 24) and any JVM 11+ target.
## Installation
Add the dependency to your `build.gradle.kts`:
```kotlin theme={"dark"}
dependencies {
implementation("ai.tuteliq:sdk:1.0.0")
}
```
Or with Gradle Groovy:
```groovy theme={"dark"}
dependencies {
implementation 'ai.tuteliq:sdk:1.0.0'
}
```
## Initialize the client
```kotlin theme={"dark"}
import ai.tuteliq.Tuteliq
val tuteliq = Tuteliq(apiKey = "YOUR_API_KEY")
```
Never hardcode API keys in source code. Use `BuildConfig` fields, the Android Keystore, or a secrets manager.
```kotlin theme={"dark"}
val tuteliq = Tuteliq(apiKey = BuildConfig.TUTELIQ_API_KEY)
```
## Detect unsafe content
Scan a single text input for harmful content across all KOSA categories.
```kotlin theme={"dark"}
val result = tuteliq.detectUnsafe(
text = "Let's meet at the park after school, don't tell your parents",
ageGroup = AgeGroup.TEN_TO_TWELVE
)
println(result.safe) // false
println(result.severity) // Severity.HIGH
println(result.categories) // [Category.GROOMING, Category.SECRECY]
```
## Detect grooming patterns
Analyze a conversation history for grooming indicators.
```kotlin theme={"dark"}
val result = tuteliq.detectGrooming(
messages = listOf(
Message(role = Role.STRANGER, text = "Hey, how old are you?"),
Message(role = Role.CHILD, text = "I'm 11"),
Message(role = Role.STRANGER, text = "Cool. Do you have your own phone?"),
Message(role = Role.STRANGER, text = "Let's talk on a different app, just us"),
),
ageGroup = AgeGroup.TEN_TO_TWELVE
)
println(result.groomingDetected) // true
println(result.riskScore) // 0.92
println(result.stage) // GroomingStage.ISOLATION
```
## Analyze emotions
Evaluate emotional well-being from conversation text.
```kotlin theme={"dark"}
val result = tuteliq.analyzeEmotions(
text = "Nobody at school talks to me anymore. I just sit alone every day.",
ageGroup = AgeGroup.THIRTEEN_TO_FIFTEEN
)
println(result.emotions) // [Emotion(label="sadness", score=0.87), ...]
println(result.distress) // true
println(result.riskLevel) // RiskLevel.ELEVATED
```
## Analyze voice
Upload an audio file for transcription and safety analysis.
```kotlin theme={"dark"}
val audioFile = File("recording.wav")
val result = tuteliq.analyzeVoice(
file = audioFile,
ageGroup = AgeGroup.THIRTEEN_TO_FIFTEEN
)
println(result.transcript)
println(result.safe)
println(result.emotions)
```
## Coroutine support
All SDK methods are `suspend` functions designed for Kotlin coroutines.
```kotlin theme={"dark"}
import kotlinx.coroutines.launch
viewModelScope.launch {
val result = tuteliq.detectUnsafe(
text = messageText,
ageGroup = AgeGroup.THIRTEEN_TO_FIFTEEN
)
_safetyState.value = result
}
```
The SDK uses OkHttp under the hood and integrates with any coroutine scope — `viewModelScope`, `lifecycleScope`, or custom scopes.
## Fraud detection and safety extended
These methods cover financial exploitation, romance scams, and coercive behaviour targeting minors. Other endpoints — `detectAppFraud`, `detectMuleRecruitment`, `detectGamblingHarm`, `detectCoerciveControl`, and `detectRadicalisation` — follow the same call pattern shown here.
### Detect social engineering
Identify manipulation tactics designed to trick a child into disclosing information or taking unsafe actions.
```kotlin theme={"dark"}
val result = tuteliq.detectSocialEngineering(
text = "If you really trusted me you'd send me your home address. All my real friends do.",
ageGroup = AgeGroup.TEN_TO_TWELVE
)
println(result.detected) // true
println(result.tactics) // [Tactic.TRUST_EXPLOITATION, Tactic.PEER_PRESSURE]
println(result.riskScore) // 0.88
```
### Detect romance scam
Analyze conversation text for romantic manipulation patterns that may indicate an adult posing as a peer.
```kotlin theme={"dark"}
val result = tuteliq.detectRomanceScam(
messages = listOf(
Message(role = Role.STRANGER, text = "I've never felt this way about anyone before. You're so mature for your age."),
Message(role = Role.CHILD, text = "Really? That makes me really happy."),
Message(role = Role.STRANGER, text = "I need you to keep us a secret. People wouldn't understand."),
),
ageGroup = AgeGroup.THIRTEEN_TO_FIFTEEN
)
println(result.detected) // true
println(result.riskScore) // 0.91
println(result.indicators) // [Indicator.LOVE_BOMBING, Indicator.SECRECY_REQUEST, Indicator.AGE_FLATTERY]
```
### Detect vulnerability exploitation
Detect attempts to identify and target emotional or situational vulnerabilities in a child.
```kotlin theme={"dark"}
val result = tuteliq.detectVulnerabilityExploitation(
text = "I know you said your parents don't listen to you. I'm different — I actually care. You can tell me anything.",
ageGroup = AgeGroup.THIRTEEN_TO_FIFTEEN
)
println(result.detected) // true
println(result.riskScore) // 0.85
println(result.vulnerabilities) // [Vulnerability.PARENTAL_CONFLICT, Vulnerability.EMOTIONAL_NEGLECT]
```
### Analyse multiple texts in one request
Run any supported detection across multiple texts in a single API call to reduce round-trips.
```kotlin theme={"dark"}
val result = tuteliq.analyseMulti(
inputs = listOf(
AnalyseMultiInput(text = "You're so special. Nobody else understands you like I do.", ageGroup = AgeGroup.THIRTEEN_TO_FIFTEEN),
AnalyseMultiInput(text = "Can you keep a secret from your mum?", ageGroup = AgeGroup.TEN_TO_TWELVE),
),
detections = listOf(Detection.SOCIAL_ENGINEERING, Detection.ROMANCE_SCAM, Detection.GROOMING)
)
println(result.results[0].detections) // AnalyseMultiDetections(socialEngineering=..., ...)
println(result.results[1].detections) // AnalyseMultiDetections(grooming=..., ...)
```
`analyseMulti` is billed per individual input × detection combination, not per request.
## Error handling
The SDK throws typed exceptions that you can catch and inspect.
```kotlin theme={"dark"}
import ai.tuteliq.TuteliqError
try {
val result = tuteliq.detectUnsafe(
text = "some content",
ageGroup = AgeGroup.TEN_TO_TWELVE
)
} catch (e: TuteliqError) {
println(e.code) // e.g. "AUTH_INVALID_KEY"
println(e.message) // human-readable description
println(e.status) // HTTP status code
}
```
## Configuration options
```kotlin theme={"dark"}
val tuteliq = Tuteliq(
apiKey = BuildConfig.TUTELIQ_API_KEY,
baseUrl = "https://api.tuteliq.ai", // default
timeout = 30_000L, // request timeout in ms
retries = 2 // automatic retries on failure
)
```
## Next steps
Explore the full API specification.
See the Swift SDK guide.
# Node.js SDK
Source: https://docs.tuteliq.ai/sdks/node
Install and use the Tuteliq Node.js SDK
The Tuteliq Node.js SDK (`@tuteliq/sdk` v2.5.0) provides a typed, promise-based client for the Tuteliq child safety API. It works in Node.js 18+ and includes full TypeScript definitions out of the box.
## Installation
```bash theme={"dark"}
npm install @tuteliq/sdk
```
## Initialize the client
```typescript theme={"dark"}
import { Tuteliq } from '@tuteliq/sdk'
const tuteliq = new Tuteliq('YOUR_API_KEY')
```
Never hardcode API keys in source code. Use environment variables or a secrets manager.
```typescript theme={"dark"}
const tuteliq = new Tuteliq(process.env.TUTELIQ_API_KEY)
```
## Detect unsafe content
Scan a single text input for harmful content across all KOSA categories.
```typescript theme={"dark"}
const result = await tuteliq.detectUnsafe({
content: "Let's meet at the park after school, don't tell your parents",
context: { ageGroup: '10-12', country: 'GB' },
})
console.log(result.unsafe) // true
console.log(result.severity) // "high"
console.log(result.categories) // ["grooming", "secrecy"]
console.log(result.risk_score) // 0.91
```
## Detect grooming patterns
Analyze a conversation history for grooming indicators. The response includes a per-message risk breakdown showing how risk escalates across the conversation.
```typescript theme={"dark"}
const result = await tuteliq.detectGrooming({
messages: [
{ role: 'stranger', content: 'Hey, how old are you?' },
{ role: 'child', content: "I'm 11" },
{ role: 'stranger', content: 'Cool. Do you have your own phone?' },
{ role: 'stranger', content: "Let's talk on a different app, just us" },
],
childAge: 11,
})
console.log(result.grooming_risk) // "high"
console.log(result.risk_score) // 0.92
console.log(result.flags) // ["isolation", "secrecy"]
// Per-message risk breakdown
if (result.message_analysis) {
for (const msg of result.message_analysis) {
console.log(`Message ${msg.message_index}: risk=${msg.risk_score}, flags=${msg.flags}`)
}
}
// Message 1: risk=0.2, flags=["information_seeking"]
// Message 2: risk=0.1, flags=[]
// Message 3: risk=0.5, flags=["information_seeking"]
// Message 4: risk=0.92, flags=["isolation", "secrecy_request"]
```
## Analyze emotions
Evaluate emotional well-being from conversation text.
```typescript theme={"dark"}
const result = await tuteliq.analyzeEmotions({
content: "Nobody at school talks to me anymore. I just sit alone every day.",
context: { ageGroup: '13-15' },
})
console.log(result.dominant_emotions) // ["sadness", "loneliness"]
console.log(result.emotion_scores) // { sadness: 0.87, loneliness: 0.75, ... }
console.log(result.trend) // "worsening"
console.log(result.recommended_followup) // "Check in about school relationships..."
```
## Analyze voice
Upload an audio file for transcription and safety analysis.
```typescript theme={"dark"}
import { readFileSync } from 'fs'
const audio = readFileSync('./recording.wav')
const result = await tuteliq.analyzeVoice({
file: audio,
filename: 'recording.wav',
analysisType: 'all',
ageGroup: '13-15',
})
console.log(result.transcription.text) // Full transcript
console.log(result.overall_severity) // "low" | "medium" | "high" | "critical"
console.log(result.overall_risk_score) // 0.0 - 1.0
console.log(result.analysis?.bullying) // Bullying analysis on transcript
```
## Fraud detection and safety extended
These methods cover financial exploitation, romance scams, and coercive behaviour targeting minors. Other endpoints — `detectAppFraud`, `detectMuleRecruitment`, `detectGamblingHarm`, `detectCoerciveControl`, and `detectRadicalisation` — follow the same call pattern shown here.
### Detect social engineering
Identify manipulation tactics designed to trick a child into disclosing information or taking unsafe actions.
```typescript theme={"dark"}
const result = await tuteliq.detectSocialEngineering({
content: "All my real friends share their address. Don't you trust me?",
context: { ageGroup: '10-12' },
})
console.log(result.detected) // true
console.log(result.categories) // [{ tag: "TRUST_EXPLOITATION", label: "Trust Exploitation", confidence: 0.92 }]
console.log(result.risk_score) // 0.88
console.log(result.recommended_action) // "Block and report to platform administrators"
```
### Detect romance scam
Analyze text for romantic manipulation patterns that may indicate an adult posing as a peer.
```typescript theme={"dark"}
const result = await tuteliq.detectRomanceScam({
content: "I've never felt this way about anyone before. You're so mature for your age. Keep us a secret.",
context: { ageGroup: '13-15' },
})
console.log(result.detected) // true
console.log(result.risk_score) // 0.91
console.log(result.categories) // [{ tag: "LOVE_BOMBING", label: "Love Bombing", confidence: 0.93 }]
console.log(result.recommended_action) // "Immediate intervention recommended"
```
### Detect vulnerability exploitation
Detect attempts to identify and target emotional or situational vulnerabilities in a child.
```typescript theme={"dark"}
const result = await tuteliq.detectVulnerabilityExploitation({
content: "I know you said your parents don't listen to you. I'm different — I actually care. You can tell me anything.",
context: { ageGroup: '13-15' },
})
console.log(result.detected) // true
console.log(result.risk_score) // 0.85
console.log(result.categories) // [{ tag: "EMOTIONAL_EXPLOITATION", label: "Emotional Exploitation", confidence: 0.88 }]
console.log(result.recommended_action) // "Flag for moderator review"
```
### Analyse multiple endpoints in one request
Run multiple detection endpoints on a single piece of content in one API call.
```typescript theme={"dark"}
const result = await tuteliq.analyseMulti({
content: "You're so special. Nobody else understands you like I do. Keep this a secret.",
detections: ['social-engineering', 'romance-scam', 'grooming'],
context: { ageGroup: '13-15' },
})
console.log(result.summary.highest_risk) // "critical"
console.log(result.summary.total_credits_used) // 3
console.log(result.results.length) // 3
```
`analyseMulti` is billed per detection endpoint, not per request.
## Error handling
The SDK throws typed errors that you can catch and inspect.
```typescript theme={"dark"}
import { Tuteliq, TuteliqError } from '@tuteliq/sdk'
try {
const result = await tuteliq.detectUnsafe({
content: 'some content',
context: { ageGroup: '10-12' },
})
} catch (error) {
if (error instanceof TuteliqError) {
console.error(error.code) // e.g. "AUTH_INVALID_KEY"
console.error(error.message) // human-readable description
console.error(error.status) // HTTP status code
}
}
```
## TypeScript support
The SDK ships with complete TypeScript definitions. No additional `@types` package is needed.
All request and response types are exported for direct use:
```typescript theme={"dark"}
import { Tuteliq } from '@tuteliq/sdk'
import type {
DetectUnsafeInput,
UnsafeResult,
DetectBullyingInput,
BullyingResult,
DetectGroomingInput,
GroomingResult,
EmotionsResult,
DetectionInput,
DetectionResult,
} from '@tuteliq/sdk'
```
## Configuration options
```typescript theme={"dark"}
const tuteliq = new Tuteliq(process.env.TUTELIQ_API_KEY, {
timeout: 30_000, // request timeout in ms (default: 30000)
retries: 3, // automatic retries on transient failures (default: 3)
retryDelay: 1000, // initial retry delay in ms (default: 1000)
})
```
## Next steps
Explore the full API specification.
See the Python SDK guide.
# Python SDK
Source: https://docs.tuteliq.ai/sdks/python
Install and use the Tuteliq Python SDK
The Tuteliq Python SDK provides both synchronous and asynchronous clients for the Tuteliq child safety API. It supports Python 3.9+.
## Installation
```bash theme={"dark"}
pip install tuteliq
```
## Initialize the client
```python theme={"dark"}
from tuteliq import Tuteliq
client = Tuteliq(api_key="YOUR_API_KEY")
```
Never hardcode API keys in source code. Use environment variables or a secrets manager.
```python theme={"dark"}
import os
from tuteliq import Tuteliq
client = Tuteliq(api_key=os.environ["TUTELIQ_API_KEY"])
```
## Detect unsafe content
Scan a single text input for harmful content across all KOSA categories.
```python theme={"dark"}
result = client.detect_unsafe(
text="Let's meet at the park after school, don't tell your parents",
age_group="10-12",
)
print(result.safe) # False
print(result.severity) # "high"
print(result.categories) # ["grooming", "secrecy"]
```
## Detect grooming patterns
Analyze a conversation history for grooming indicators.
```python theme={"dark"}
result = client.detect_grooming(
messages=[
{"role": "stranger", "text": "Hey, how old are you?"},
{"role": "child", "text": "I'm 11"},
{"role": "stranger", "text": "Cool. Do you have your own phone?"},
{"role": "stranger", "text": "Let's talk on a different app, just us"},
],
age_group="10-12",
)
print(result.grooming_detected) # True
print(result.risk_score) # 0.92
print(result.stage) # "isolation"
```
## Analyze emotions
Evaluate emotional well-being from conversation text.
```python theme={"dark"}
result = client.analyze_emotions(
text="Nobody at school talks to me anymore. I just sit alone every day.",
age_group="13-15",
)
print(result.emotions) # [{"label": "sadness", "score": 0.87}, ...]
print(result.distress) # True
print(result.risk_level) # "elevated"
```
## Analyze voice
Upload an audio file for transcription and safety analysis.
```python theme={"dark"}
result = client.analyze_voice(
file=open("recording.wav", "rb"),
age_group="13-15",
)
print(result.transcript)
print(result.safe)
print(result.emotions)
```
## Fraud detection and safety extended
These methods cover financial exploitation, romance scams, and coercive behaviour targeting minors. Other endpoints — `detect_app_fraud`, `detect_mule_recruitment`, `detect_gambling_harm`, `detect_coercive_control`, and `detect_radicalisation` — follow the same call pattern shown here.
### Detect social engineering
Identify manipulation tactics designed to trick a child into disclosing information or taking unsafe actions.
```python theme={"dark"}
result = client.detect_social_engineering(
text="If you really trusted me you'd send me your home address. All my real friends do.",
age_group="10-12",
)
print(result.detected) # True
print(result.tactics) # ["trust_exploitation", "peer_pressure"]
print(result.risk_score) # 0.88
```
### Detect romance scam
Analyze conversation text for romantic manipulation patterns that may indicate an adult posing as a peer.
```python theme={"dark"}
result = client.detect_romance_scam(
messages=[
{"role": "stranger", "text": "I've never felt this way about anyone before. You're so mature for your age."},
{"role": "child", "text": "Really? That makes me really happy."},
{"role": "stranger", "text": "I need you to keep us a secret. People wouldn't understand."},
],
age_group="13-15",
)
print(result.detected) # True
print(result.risk_score) # 0.91
print(result.indicators) # ["love_bombing", "secrecy_request", "age_flattery"]
```
### Detect vulnerability exploitation
Detect attempts to identify and target emotional or situational vulnerabilities in a child.
```python theme={"dark"}
result = client.detect_vulnerability_exploitation(
text="I know you said your parents don't listen to you. I'm different — I actually care. You can tell me anything.",
age_group="13-15",
)
print(result.detected) # True
print(result.risk_score) # 0.85
print(result.vulnerabilities) # ["parental_conflict", "emotional_neglect"]
```
### Analyse multiple texts in one request
Run any supported detection across multiple texts in a single API call to reduce round-trips.
```python theme={"dark"}
result = client.analyse_multi(
inputs=[
{"text": "You're so special. Nobody else understands you like I do.", "age_group": "13-15"},
{"text": "Can you keep a secret from your mum?", "age_group": "10-12"},
],
detections=["social-engineering", "romance-scam", "grooming"],
)
print(result.results[0].detections) # {"social_engineering": {"detected": True, ...}, ...}
print(result.results[1].detections) # {"grooming": {"detected": True, ...}, ...}
```
`analyse_multi` is billed per individual input × detection combination, not per request.
## Async support
For asynchronous applications, use the `AsyncTuteliq` client. It exposes the same methods with `await` syntax.
```python theme={"dark"}
import asyncio
from tuteliq import AsyncTuteliq
client = AsyncTuteliq(api_key=os.environ["TUTELIQ_API_KEY"])
async def main():
result = await client.detect_unsafe(
text="example message to analyze",
age_group="13-15",
)
print(result.safe)
asyncio.run(main())
```
`AsyncTuteliq` is ideal for frameworks like FastAPI, aiohttp, and Django with ASGI. It uses `httpx` under the hood.
## Error handling
The SDK raises typed exceptions that you can catch and inspect.
```python theme={"dark"}
from tuteliq import Tuteliq, TuteliqError
client = Tuteliq(api_key="YOUR_API_KEY")
try:
result = client.detect_unsafe(
text="some content",
age_group="10-12",
)
except TuteliqError as e:
print(e.code) # e.g. "AUTH_INVALID_KEY"
print(e.message) # human-readable description
print(e.status) # HTTP status code
```
## Configuration options
```python theme={"dark"}
client = Tuteliq(
api_key=os.environ["TUTELIQ_API_KEY"],
base_url="https://api.tuteliq.ai", # default
timeout=30.0, # request timeout in seconds
retries=2, # automatic retries on failure
)
```
## Next steps
Explore the full API specification.
See the Node.js SDK guide.
# React Native SDK
Source: https://docs.tuteliq.ai/sdks/react-native
Install and use the Tuteliq React Native SDK
The Tuteliq React Native SDK provides a typed client for the Tuteliq child safety API in React Native applications. It supports React Native 0.70+ with full TypeScript definitions.
## Installation
```bash theme={"dark"}
npm install @tuteliq/react-native
```
Or with Yarn:
```bash theme={"dark"}
yarn add @tuteliq/react-native
```
## Initialize the client
```typescript theme={"dark"}
import { Tuteliq } from '@tuteliq/react-native';
const tuteliq = new Tuteliq('YOUR_API_KEY');
```
Never hardcode API keys in source code. Use `react-native-config`, environment variables, or a secrets manager.
```typescript theme={"dark"}
import Config from 'react-native-config';
const tuteliq = new Tuteliq(Config.TUTELIQ_API_KEY);
```
## Detect unsafe content
Scan a single text input for harmful content across all KOSA categories.
```typescript theme={"dark"}
const result = await tuteliq.detectUnsafe({
content: "Let's meet at the park after school, don't tell your parents",
context: { ageGroup: '10-12' },
});
console.log(result.unsafe); // true
console.log(result.severity); // "high"
console.log(result.categories); // ["grooming", "secrecy"]
console.log(result.risk_score); // 0.91
```
## Detect grooming patterns
Analyze a conversation history for grooming indicators.
```typescript theme={"dark"}
const result = await tuteliq.detectGrooming({
messages: [
{ role: 'stranger', content: 'Hey, how old are you?' },
{ role: 'child', content: "I'm 11" },
{ role: 'stranger', content: 'Cool. Do you have your own phone?' },
{ role: 'stranger', content: "Let's talk on a different app, just us" },
],
childAge: 11,
});
console.log(result.grooming_risk); // "high"
console.log(result.risk_score); // 0.92
console.log(result.flags); // ["isolation", "secrecy"]
```
## Analyze emotions
Evaluate emotional well-being from conversation text.
```typescript theme={"dark"}
const result = await tuteliq.analyzeEmotions({
content: 'Nobody at school talks to me anymore. I just sit alone every day.',
context: { ageGroup: '13-15' },
});
console.log(result.dominant_emotions); // ["sadness", "loneliness"]
console.log(result.emotion_scores); // { sadness: 0.87, loneliness: 0.75, ... }
console.log(result.trend); // "worsening"
console.log(result.recommended_followup); // "Check in about school relationships..."
```
## Analyze voice
Upload an audio file for transcription and safety analysis.
```typescript theme={"dark"}
import RNFS from 'react-native-fs';
const audioPath = `${RNFS.DocumentDirectoryPath}/recording.wav`;
const result = await tuteliq.analyzeVoice({
filePath: audioPath,
filename: 'recording.wav',
analysisType: 'all',
ageGroup: '13-15',
});
console.log(result.transcription.text); // Full transcript
console.log(result.overall_severity); // "low" | "medium" | "high" | "critical"
console.log(result.overall_risk_score); // 0.0 - 1.0
```
## Hook integration
The SDK exports a `useTuteliq` hook for convenient usage within React components.
```typescript theme={"dark"}
import { useTuteliq } from '@tuteliq/react-native';
function SafetyScreen() {
const tuteliq = useTuteliq();
const [result, setResult] = useState(null);
const checkMessage = async (text: string) => {
const res = await tuteliq.detectUnsafe({
content: text,
context: { ageGroup: '13-15' },
});
setResult(res);
};
return (
{result?.unsafe === false ? 'Content is safe' : 'Checking...'}
);
}
```
## Fraud detection and safety extended
These methods cover financial exploitation, romance scams, and coercive behaviour targeting minors. Other endpoints — `detectAppFraud`, `detectMuleRecruitment`, `detectGamblingHarm`, `detectCoerciveControl`, and `detectRadicalisation` — follow the same call pattern shown here.
### Detect social engineering
Identify manipulation tactics designed to trick a child into disclosing information or taking unsafe actions.
```typescript theme={"dark"}
const result = await tuteliq.detectSocialEngineering({
content: "If you really trusted me you'd send me your home address. All my real friends do.",
context: { ageGroup: '10-12' },
});
console.log(result.detected); // true
console.log(result.categories); // [{ tag: "TRUST_EXPLOITATION", label: "Trust Exploitation", confidence: 0.92 }]
console.log(result.risk_score); // 0.88
console.log(result.recommended_action); // "Block and report to platform administrators"
```
### Detect romance scam
Analyze text for romantic manipulation patterns that may indicate an adult posing as a peer.
```typescript theme={"dark"}
const result = await tuteliq.detectRomanceScam({
content: "I've never felt this way about anyone before. You're so mature for your age. Keep us a secret.",
context: { ageGroup: '13-15' },
});
console.log(result.detected); // true
console.log(result.risk_score); // 0.91
console.log(result.categories); // [{ tag: "LOVE_BOMBING", label: "Love Bombing", confidence: 0.93 }]
console.log(result.recommended_action); // "Immediate intervention recommended"
```
### Detect vulnerability exploitation
Detect attempts to identify and target emotional or situational vulnerabilities in a child.
```typescript theme={"dark"}
const result = await tuteliq.detectVulnerabilityExploitation({
content: "I know you said your parents don't listen to you. I'm different — I actually care. You can tell me anything.",
context: { ageGroup: '13-15' },
});
console.log(result.detected); // true
console.log(result.risk_score); // 0.85
console.log(result.categories); // [{ tag: "EMOTIONAL_EXPLOITATION", label: "Emotional Exploitation", confidence: 0.88 }]
console.log(result.recommended_action); // "Flag for moderator review"
```
### Analyse multiple endpoints in one request
Run multiple detection endpoints on a single piece of content in one API call.
```typescript theme={"dark"}
const result = await tuteliq.analyseMulti({
content: "You're so special. Nobody else understands you like I do. Keep this a secret.",
detections: ['social-engineering', 'romance-scam', 'grooming'],
context: { ageGroup: '13-15' },
});
console.log(result.summary.highest_risk); // "critical"
console.log(result.summary.total_credits_used); // 3
console.log(result.results.length); // 3
```
`analyseMulti` is billed per detection endpoint, not per request.
## Error handling
The SDK throws typed errors that you can catch and inspect.
```typescript theme={"dark"}
import { Tuteliq, TuteliqError } from '@tuteliq/react-native';
try {
const result = await tuteliq.detectUnsafe({
content: 'some content',
context: { ageGroup: '10-12' },
});
} catch (error) {
if (error instanceof TuteliqError) {
console.error(error.code); // e.g. "AUTH_INVALID_KEY"
console.error(error.message); // human-readable description
console.error(error.status); // HTTP status code
}
}
```
## Configuration options
```typescript theme={"dark"}
const tuteliq = new Tuteliq(Config.TUTELIQ_API_KEY, {
timeout: 30_000, // request timeout in ms (default: 30000)
retries: 3, // automatic retries on transient failures (default: 3)
retryDelay: 1000, // initial retry delay in ms (default: 1000)
});
```
## Next steps
Explore the full API specification.
See the Node.js SDK guide.
# Swift SDK
Source: https://docs.tuteliq.ai/sdks/swift
Install and use the Tuteliq Swift SDK
The Tuteliq Swift SDK provides a native client for the Tuteliq child safety API using Swift concurrency (`async`/`await`). It supports iOS 15.0+ and macOS 12.0+.
## Installation
Add the Tuteliq package via Swift Package Manager in Xcode:
1. Go to **File > Add Package Dependencies**.
2. Enter the repository URL:
```
https://github.com/Tuteliq/swift
```
3. Select your desired version rule and add the package to your target.
Alternatively, add it to your `Package.swift`:
```swift theme={"dark"}
dependencies: [
.package(url: "https://github.com/Tuteliq/swift", from: "1.0.0"),
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "Tuteliq", package: "swift"),
]
),
]
```
## Deployment targets
| Platform | Minimum Version |
| -------- | --------------- |
| iOS | 15.0 |
| macOS | 12.0 |
## Initialize the client
```swift theme={"dark"}
import Tuteliq
let tuteliq = Tuteliq(apiKey: "YOUR_API_KEY")
```
Never hardcode API keys in source code. Store them in the Keychain, Xcode build configuration, or a secrets manager.
```swift theme={"dark"}
let tuteliq = Tuteliq(apiKey: Configuration.tuteliqApiKey)
```
## Detect unsafe content
Scan a single text input for harmful content across all KOSA categories.
```swift theme={"dark"}
let result = try await tuteliq.detectUnsafe(
text: "Let's meet at the park after school, don't tell your parents",
ageGroup: .tenToTwelve
)
print(result.safe) // false
print(result.severity) // .high
print(result.categories) // [.grooming, .secrecy]
```
## Detect grooming patterns
Analyze a conversation history for grooming indicators.
```swift theme={"dark"}
let messages: [Message] = [
Message(role: .stranger, text: "Hey, how old are you?"),
Message(role: .child, text: "I'm 11"),
Message(role: .stranger, text: "Cool. Do you have your own phone?"),
Message(role: .stranger, text: "Let's talk on a different app, just us"),
]
let result = try await tuteliq.detectGrooming(
messages: messages,
ageGroup: .tenToTwelve
)
print(result.groomingDetected) // true
print(result.riskScore) // 0.92
print(result.stage) // .isolation
```
## Analyze emotions
Evaluate emotional well-being from conversation text.
```swift theme={"dark"}
let result = try await tuteliq.analyzeEmotions(
text: "Nobody at school talks to me anymore. I just sit alone every day.",
ageGroup: .thirteenToFifteen
)
print(result.emotions) // [Emotion(label: "sadness", score: 0.87), ...]
print(result.distress) // true
print(result.riskLevel) // .elevated
```
## Analyze voice
Upload an audio file for transcription and safety analysis.
```swift theme={"dark"}
let audioURL = Bundle.main.url(forResource: "recording", withExtension: "wav")!
let audioData = try Data(contentsOf: audioURL)
let result = try await tuteliq.analyzeVoice(
file: audioData,
ageGroup: .thirteenToFifteen
)
print(result.transcript)
print(result.safe)
print(result.emotions)
```
## Fraud detection and safety extended
These methods cover financial exploitation, romance scams, and coercive behaviour targeting minors. Other endpoints — `detectAppFraud`, `detectMuleRecruitment`, `detectGamblingHarm`, `detectCoerciveControl`, and `detectRadicalisation` — follow the same call pattern shown here.
### Detect social engineering
Identify manipulation tactics designed to trick a child into disclosing information or taking unsafe actions.
```swift theme={"dark"}
let result = try await tuteliq.detectSocialEngineering(
text: "If you really trusted me you'd send me your home address. All my real friends do.",
ageGroup: .tenToTwelve
)
print(result.detected) // true
print(result.tactics) // [.trustExploitation, .peerPressure]
print(result.riskScore) // 0.88
```
### Detect romance scam
Analyze conversation text for romantic manipulation patterns that may indicate an adult posing as a peer.
```swift theme={"dark"}
let messages: [Message] = [
Message(role: .stranger, text: "I've never felt this way about anyone before. You're so mature for your age."),
Message(role: .child, text: "Really? That makes me really happy."),
Message(role: .stranger, text: "I need you to keep us a secret. People wouldn't understand."),
]
let result = try await tuteliq.detectRomanceScam(
messages: messages,
ageGroup: .thirteenToFifteen
)
print(result.detected) // true
print(result.riskScore) // 0.91
print(result.indicators) // [.loveBombing, .secrecyRequest, .ageFlattery]
```
### Detect vulnerability exploitation
Detect attempts to identify and target emotional or situational vulnerabilities in a child.
```swift theme={"dark"}
let result = try await tuteliq.detectVulnerabilityExploitation(
text: "I know you said your parents don't listen to you. I'm different — I actually care. You can tell me anything.",
ageGroup: .thirteenToFifteen
)
print(result.detected) // true
print(result.riskScore) // 0.85
print(result.vulnerabilities) // [.parentalConflict, .emotionalNeglect]
```
### Analyse multiple texts in one request
Run any supported detection across multiple texts in a single API call to reduce round-trips.
```swift theme={"dark"}
let result = try await tuteliq.analyseMulti(
inputs: [
AnalyseMultiInput(text: "You're so special. Nobody else understands you like I do.", ageGroup: .thirteenToFifteen),
AnalyseMultiInput(text: "Can you keep a secret from your mum?", ageGroup: .tenToTwelve),
],
detections: [.socialEngineering, .romanceScam, .grooming]
)
print(result.results[0].detections) // AnalyseMultiDetections(socialEngineering: ..., ...)
print(result.results[1].detections) // AnalyseMultiDetections(grooming: ..., ...)
```
`analyseMulti` is billed per individual input × detection combination, not per request.
## Error handling
The SDK throws typed `TuteliqError` values that you can pattern-match on.
```swift theme={"dark"}
do {
let result = try await tuteliq.detectUnsafe(
text: "some content",
ageGroup: .tenToTwelve
)
} catch let error as TuteliqError {
print(error.code) // e.g. .authInvalidKey
print(error.message) // human-readable description
print(error.status) // HTTP status code
} catch {
print("Unexpected error: \(error)")
}
```
## Configuration options
```swift theme={"dark"}
let tuteliq = Tuteliq(
apiKey: Configuration.tuteliqApiKey,
baseURL: URL(string: "https://api.tuteliq.ai")!, // default
timeout: 30, // request timeout in seconds
retries: 2 // automatic retries on failure
)
```
## SwiftUI integration
The SDK works seamlessly with SwiftUI. All methods are `async` and can be called directly from `.task` modifiers or `@MainActor` contexts.
```swift theme={"dark"}
struct ContentModerationView: View {
@State private var isSafe: Bool?
let tuteliq = Tuteliq(apiKey: Configuration.tuteliqApiKey)
var body: some View {
Text(isSafe == true ? "Content is safe" : "Checking...")
.task {
let result = try? await tuteliq.detectUnsafe(
text: messageText,
ageGroup: .thirteenToFifteen
)
isSafe = result?.safe
}
}
}
```
## Next steps
Explore the full API specification.
See the Node.js SDK guide.
# Unity SDK
Source: https://docs.tuteliq.ai/sdks/unity
Install and use the Tuteliq Unity SDK
The Tuteliq Unity SDK provides a client for the Tuteliq child safety API designed for Unity 2021.3 LTS and later. It supports both coroutine and async/await patterns.
## Installation
### Unity Package Manager
1. Open **Window > Package Manager**.
2. Click **+** and select **Add package from git URL**.
3. Enter:
```
https://github.com/Tuteliq/unity.git
```
### Manual installation
Download the latest `.unitypackage` from the [GitHub releases](https://github.com/Tuteliq/unity/releases) and import it into your project.
## Initialize the client
```csharp theme={"dark"}
using Tuteliq;
var tuteliq = new TuteliqClient("YOUR_API_KEY");
```
Never hardcode API keys in source code. Use ScriptableObject configs, environment variables, or Unity's built-in encryption for key storage.
```csharp theme={"dark"}
[SerializeField] private TuteliqConfig config;
void Start()
{
var tuteliq = new TuteliqClient(config.ApiKey);
}
```
## Detect unsafe content
Scan a single text input for harmful content across all KOSA categories.
```csharp theme={"dark"}
var result = await tuteliq.DetectUnsafeAsync(
text: "Let's meet at the park after school, don't tell your parents",
ageGroup: AgeGroup.TenToTwelve
);
Debug.Log(result.Safe); // false
Debug.Log(result.Severity); // Severity.High
Debug.Log(result.Categories); // [Category.Grooming, Category.Secrecy]
```
## Detect grooming patterns
Analyze a conversation history for grooming indicators.
```csharp theme={"dark"}
var result = await tuteliq.DetectGroomingAsync(
messages: new[]
{
new Message(Role.Stranger, "Hey, how old are you?"),
new Message(Role.Child, "I'm 11"),
new Message(Role.Stranger, "Cool. Do you have your own phone?"),
new Message(Role.Stranger, "Let's talk on a different app, just us"),
},
ageGroup: AgeGroup.TenToTwelve
);
Debug.Log(result.GroomingDetected); // true
Debug.Log(result.RiskScore); // 0.92
Debug.Log(result.Stage); // GroomingStage.Isolation
```
## Analyze emotions
Evaluate emotional well-being from conversation text.
```csharp theme={"dark"}
var result = await tuteliq.AnalyzeEmotionsAsync(
text: "Nobody at school talks to me anymore. I just sit alone every day.",
ageGroup: AgeGroup.ThirteenToFifteen
);
Debug.Log(result.Emotions); // [Emotion { Label = "sadness", Score = 0.87 }, ...]
Debug.Log(result.Distress); // true
Debug.Log(result.RiskLevel); // RiskLevel.Elevated
```
## Analyze voice
Analyze in-game voice chat for safety concerns.
```csharp theme={"dark"}
var audioClip = Microphone.Start(null, false, 10, 16000);
// ... record audio ...
Microphone.End(null);
var audioData = AudioClipToWav(audioClip);
var result = await tuteliq.AnalyzeVoiceAsync(
file: audioData,
ageGroup: AgeGroup.ThirteenToFifteen
);
Debug.Log(result.Transcript);
Debug.Log(result.Safe);
Debug.Log(result.Emotions);
```
## Fraud detection
Detect financial exploitation and scam patterns in in-game or social features. Other methods — `DetectAppFraudAsync`, `DetectMuleRecruitmentAsync` — follow the same pattern.
### Detect social engineering
```csharp theme={"dark"}
var result = await tuteliq.DetectSocialEngineeringAsync(
text: "If you really trusted me you'd share your account password. All my friends do.",
ageGroup: AgeGroup.TenToTwelve
);
Debug.Log(result.Detected); // true
Debug.Log(result.RiskScore); // 0.88
Debug.Log(result.Level); // "high"
```
### Detect romance scam
```csharp theme={"dark"}
var result = await tuteliq.DetectRomanceScamAsync(
text: "I've never felt this way about anyone. You're so mature. Keep us a secret.",
ageGroup: AgeGroup.ThirteenToFifteen
);
Debug.Log(result.Detected); // true
Debug.Log(result.RiskScore); // 0.91
```
## Safety extended
Detect extended safety threats including gambling harm, coercive control, vulnerability exploitation, and radicalisation.
### Detect gambling harm
```csharp theme={"dark"}
var result = await tuteliq.DetectGamblingHarmAsync(
text: "I know a way to get free V-Bucks. Just put in your parent's card and I'll double it.",
ageGroup: AgeGroup.TenToTwelve
);
Debug.Log(result.Detected); // true
Debug.Log(result.RiskScore); // 0.82
```
### Detect vulnerability exploitation
```csharp theme={"dark"}
var result = await tuteliq.DetectVulnerabilityExploitationAsync(
text: "I know you said your parents don't listen. I'm different — I actually care.",
ageGroup: AgeGroup.ThirteenToFifteen
);
Debug.Log(result.Detected); // true
Debug.Log(result.RiskScore); // 0.85
```
## Multi-endpoint analysis
Run multiple detection endpoints on a single text in one API call.
```csharp theme={"dark"}
var result = await tuteliq.AnalyseMultiAsync(
text: "You're so special. Nobody understands you like I do. Send me a photo.",
detections: new[] { Detection.SocialEngineering, Detection.Grooming, Detection.RomanceScam },
ageGroup: AgeGroup.ThirteenToFifteen
);
Debug.Log(result.Summary.OverallRiskLevel); // "high"
Debug.Log(result.Summary.DetectedCount); // 2
foreach (var r in result.Results)
{
Debug.Log($"{r.Endpoint}: {r.Detected} (risk: {r.RiskScore})");
}
```
`AnalyseMultiAsync` is billed per individual detection endpoint, not per request.
## Analyze video
Upload a video file for frame-by-frame safety analysis.
```csharp theme={"dark"}
var videoBytes = await File.ReadAllBytesAsync("clip.mp4");
var result = await tuteliq.AnalyzeVideoAsync(
file: videoBytes,
filename: "clip.mp4",
ageGroup: AgeGroup.ThirteenToFifteen
);
Debug.Log(result.FramesAnalyzed);
Debug.Log(result.OverallRiskScore);
foreach (var finding in result.SafetyFindings)
{
Debug.Log($"Frame {finding.FrameIndex}: {finding.Description} ({finding.Severity})");
}
```
## Coroutine support
For projects that prefer coroutines over async/await:
```csharp theme={"dark"}
using UnityEngine;
using Tuteliq;
public class ChatModerator : MonoBehaviour
{
private TuteliqClient _tuteliq;
void Start()
{
_tuteliq = new TuteliqClient(config.ApiKey);
}
public void CheckMessage(string text)
{
StartCoroutine(_tuteliq.DetectUnsafe(
text: text,
ageGroup: AgeGroup.ThirteenToFifteen,
onComplete: result =>
{
if (!result.Safe)
{
Debug.LogWarning($"Unsafe content: {result.Severity}");
// Block message, notify moderator, etc.
}
}
));
}
}
```
Both coroutine and async/await patterns use the same underlying HTTP client. Choose whichever fits your project architecture.
## Error handling
The SDK throws typed exceptions that you can catch and inspect.
```csharp theme={"dark"}
try
{
var result = await tuteliq.DetectUnsafeAsync(
text: "some content",
ageGroup: AgeGroup.TenToTwelve
);
}
catch (TuteliqException ex)
{
Debug.LogError($"{ex.Code}: {ex.Message}");
}
```
## Configuration options
```csharp theme={"dark"}
var tuteliq = new TuteliqClient(new TuteliqOptions
{
ApiKey = config.ApiKey,
BaseUrl = "https://api.tuteliq.ai", // default
Timeout = TimeSpan.FromSeconds(30), // request timeout
Retries = 2 // automatic retries on failure
});
```
## Next steps
Explore the full API specification.
See the .NET SDK guide.
# Secure Development Lifecycle
Source: https://docs.tuteliq.ai/sdlc
How Tuteliq develops, reviews, tests, and deploys code to production
## Overview
Tuteliq follows a structured development lifecycle to ensure all code changes are reviewed, tested, and safely deployed to production. This document describes the end-to-end process from development to production release.
## Branch Strategy
Tuteliq uses a **trunk-based development** model with short-lived feature branches:
| Branch | Purpose | Protection |
| ---------- | ------------------------- | ----------------------------------------- |
| `main` | Production-ready code | Protected — no direct pushes, requires PR |
| `feat/*` | New features | Developer branch |
| `fix/*` | Bug fixes | Developer branch |
| `hotfix/*` | Critical production fixes | Expedited review process |
## Code Merge Process
All changes begin on a dedicated branch created from `main`. Branch naming follows the convention `feat/description`, `fix/description`, or `hotfix/description`.
```bash theme={"dark"}
git checkout -b feat/add-new-detection-endpoint
```
Developers write code and run the full test suite locally before pushing:
* Unit tests
* Integration tests
* Type checking (TypeScript strict mode, mypy, swiftformat, dart analyze)
* Linting (ESLint, Prettier, SwiftFormat, Ruff)
All code must pass local CI checks before a pull request is created.
A pull request is opened against `main` with:
* Clear description of the change and its purpose
* Link to the relevant issue or task
* Test plan describing how the change was verified
* Screenshots or examples for UI or API response changes
Every pull request triggers automated CI pipelines that must pass before merge:
* **Build** — Compilation and type checking across all supported targets
* **Test** — Full unit and integration test suite
* **Lint** — Code style and formatting enforcement
* **Security** — Dependency vulnerability scanning (Dependabot / GitHub Advisory Database)
Pull requests cannot be merged if any CI check fails.
All pull requests require at least **one approving review** from a team member before merge. Reviewers evaluate:
* Correctness and logic
* Security implications (injection, auth bypass, data exposure)
* Performance impact
* Test coverage for new or changed functionality
* API contract and backward compatibility
* Adherence to coding standards and project conventions
After CI passes and review is approved, the pull request is merged into `main` using **squash merge** to maintain a clean commit history. The feature branch is automatically deleted after merge.
Merges to `main` trigger automatic deployment to production via the CI/CD pipeline:
* **API** — Deployed to Google Cloud Run with zero-downtime rolling updates
* **SDKs** — Published to respective package registries (npm, PyPI, pub.dev, Maven Central, NuGet, Swift Package Index) on tagged releases
* **Documentation** — Auto-deployed via Mintlify on push to `main`
Deployments are monitored for errors and latency regressions. Automatic rollback is triggered if health checks fail.
## Hotfix Process
For critical production issues:
1. A `hotfix/*` branch is created directly from `main`
2. The fix is implemented with targeted tests
3. An expedited code review is performed
4. The hotfix is merged and deployed immediately
5. A post-incident review is conducted within 24 hours
## Security Controls
| Control | Implementation |
| ----------------------- | ------------------------------------------------------------------------- |
| **Branch protection** | `main` requires PR, passing CI, and approving review |
| **Dependency scanning** | Automated via GitHub Dependabot with weekly scans |
| **Secret detection** | Pre-commit hooks and CI checks prevent secrets from being committed |
| **Access control** | Repository access follows least-privilege principle |
| **Audit trail** | All changes tracked via Git history and GitHub audit log |
| **Signed releases** | SDK releases are tagged and published through CI with verified provenance |
## Environment Promotion
Code progresses through environments before reaching production:
| Environment | Purpose | Trigger |
| --------------- | ----------------------------- | ------------------------------------------ |
| **Development** | Local development and testing | Developer machine |
| **Staging** | Pre-production validation | Push to `main` (pre-deploy step) |
| **Production** | Live customer traffic | Automatic after staging health checks pass |
All environments use isolated infrastructure, separate API keys, and independent databases. Production credentials are never accessible from development or staging.
## Monitoring & Rollback
* **Health checks** — Automated HTTP health probes on every deployment
* **Error tracking** — Real-time error monitoring with alerting
* **Latency monitoring** — P50/P95/P99 latency tracked per endpoint
* **Automatic rollback** — Cloud Run reverts to the previous revision if the new deployment fails health checks
* **Manual rollback** — Any team member can trigger an immediate rollback via the deployment dashboard
## Questions
For questions about our development process or to request additional documentation, contact [security@tuteliq.ai](mailto:security@tuteliq.ai).
# Synthetic Content Detection
Source: https://docs.tuteliq.ai/synthetic-content
Multi-signal forensic detection of AI-generated text, images, audio, and video — deepfakes, voice cloning, synthetic identities, and AI-enhanced child exploitation content
Tuteliq detects AI-generated and synthetic content across all four modalities — text, image, audio, and video. Each endpoint classifies content using the standardized taxonomy from the [Child Protection Blueprint](https://protectingchildrenonline.org): `confirmed_synthetic`, `suspected_synthetic`, `unknown`, or `confirmed_authentic`.
For images and video, Tuteliq goes far beyond a single model call. The **multi-signal forensic pipeline** runs up to 6 independent analysis engines in parallel — vision forensics, EXIF metadata, pixel statistics, C2PA Content Credentials, watermark detection, and perceptual hashing — then aggregates them into a weighted ensemble assessment. Any single engine can fail without degrading the result.
Our own forensic vision models, EXIF metadata, pixel statistics, C2PA Content Credentials, frequency-domain watermark analysis, and perceptual hash matching — all in parallel.
Frame-by-frame face identity tracking, landmark stability analysis, and audio-visual lip-sync correlation to catch deepfakes that single-frame analysis misses.
Mel spectrogram analysis on our own EU-hosted vision models, plus quantitative audio statistics (dynamic range, silence ratio, flat factor) to detect synthetic speech beyond transcript analysis.
## Classification Levels
| Classification | Description |
| --------------------- | ---------------------------------------------- |
| `confirmed_synthetic` | High confidence AI-generated content detected |
| `suspected_synthetic` | Moderate indicators of synthetic content |
| `unknown` | Insufficient data to determine authenticity |
| `confirmed_authentic` | High confidence genuine, human-created content |
## Category Taxonomy
| Tag | Description |
| ------------------------- | --------------------------------------------------------- |
| `AI_GENERATED_TEXT` | LLM-generated text (ChatGPT, Claude, etc.) |
| `AI_GENERATED_IMAGE` | AI-generated image (Midjourney, DALL-E, Stable Diffusion) |
| `AI_GENERATED_AUDIO` | Voice cloning, text-to-speech synthesis |
| `AI_GENERATED_VIDEO` | Fully AI-generated video content |
| `AI_MANIPULATED_MEDIA` | Deepfakes, face swaps, manipulated media |
| `SYNTHETIC_IDENTITY` | Fake identity created using AI tools |
| `SYNTHETIC_CSAM` | AI-generated child sexual abuse material |
| `SYNTHETIC_IMPERSONATION` | AI-generated impersonation of a real person |
| `AI_ENHANCED_GROOMING` | AI-assisted grooming scripts |
| `AI_ENHANCED_SEXTORTION` | AI-assisted sextortion content |
`SYNTHETIC_CSAM` always escalates to severity 1.0, level `critical`, and recommended action `immediate_intervention` regardless of confidence score.
### Detecting CSAM risk without CSAM training data
Hash matching (PhotoDNA, the IWF Hash List) catches material that has already been
reported and catalogued. It cannot catch anything new. We approach the problem
differently, through **compositional signal analysis**: several independent models
each answer a narrow, legitimate question, and risk is inferred from how their
answers combine.
**Tuteliq never trains on, stores, or accesses real CSAM imagery.** That is a
deliberate architectural constraint rather than a limitation, and it is what makes
it possible to identify new, previously unseen exploitative content that has never
been reported, hashed or catalogued.
| Signal | What it does |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Age estimation** | Our own convolutional pipeline with multi-face detection. Applies a conservative ±3 year buffer biased toward the younger estimate. |
| **Anatomical detection** | Our own ONNX model covering 18 body-part classes with severity levels. Anatomical findings override abstract scoring. |
| **Skin exposure analysis** | HSV segmentation with body-zone distribution and intimate-region concentration detection. |
| **Benign context recognition** | Recognises bath, beach and pool settings, with age-tiered thresholds: under-6 requires 90% NSFW confidence, while 13-17 requires only 30%. |
Every one of these runs on models we train and host ourselves. No image is sent to
a third-party vision API, and no image is retained once the request completes.
This is **complementary to hash-based systems, not a replacement**. They catch
known CSAM; this catches unknown CSAM. Run both.
#### Knowing which layers ran: `sources`
Every `csam_assessment` reports the detection layers that produced it:
```json theme={"dark"}
"csam_assessment": {
"risk_level": "low",
"risk_score": 0.0435,
"requires_escalation": false,
"requires_review": false,
"sources": ["tuteliq_compositional"],
"signals": [],
"age_signal": { "...": "..." }
}
```
| Source | Method | Availability |
| ----------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------- |
| `tuteliq_compositional` | Our own models: age estimation, anatomical detection, skin exposure, benign context | All plans |
| `iwf_hash_list` | Hash matching against the IWF Hash List | **IWF members only.** Requires your own IWF membership. |
The naming convention is `{authority}_{method}`: the prefix is the organisation
whose system made the assertion, the suffix is how. That is deliberate, because
an assessment can become evidence, and the question asked of it later is
provenance.
A source is listed when it **ran**, not when it matched. `iwf_hash_list`
appearing with no corresponding signal means the known-material list was
checked and came back clean, which is a stronger statement than its absence.
`sources` is **reported, not requested.** There is no parameter to switch a
detection layer off, and which layers run is determined by your account
entitlement rather than by the request. Adding a source is a non-breaking
change: responses simply become richer, and no client code has to change.
#### Matching against the IWF Hash List
If your organisation is an **Internet Watch Foundation member**, you can pair the
compositional analysis above with hash matching against the IWF Hash List, so
known and unknown material are both covered. The forensic pipeline returns a
`perceptual_hash` on every image response for exactly this purpose.
The two layers answer different questions and neither replaces the other:
| | Catches | Misses |
| -------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ |
| **IWF Hash List matching** | Material already reported and catalogued, with very high precision | Anything new, and anything altered enough to change the hash |
| **Compositional analysis** | New, previously unseen material, and altered variants | Nothing catalogued that it cannot see signals for |
Membership gives you access to the Hash List, the URL List and IWF's reporting
infrastructure. If you operate a platform where children are present, we
strongly encourage joining. See how to join.
***
## Text Detection
Analyzes text for AI-generated content indicators — LLM-generated text, synthetic identities, AI-enhanced grooming scripts, and more.
```
POST /api/v1/safety/synthetic-content
Content-Type: application/json
```
### Request
```json theme={"dark"}
{
"text": "In conclusion, it is important to note that there are several key factors to consider when evaluating the implications of this complex issue.",
"context": {
"language": "en",
"age_group": "13-15",
"platform": "discord"
},
"external_id": "msg_123",
"customer_id": "user_456"
}
```
### Response
```json theme={"dark"}
{
"endpoint": "synthetic-content",
"detected": true,
"severity": 0.8,
"level": "high",
"classification": "suspected_synthetic",
"confidence": 0.9,
"risk_score": 0.7,
"categories": [
{ "tag": "AI_GENERATED_TEXT", "label": "AI-generated text", "confidence": 0.9 }
],
"evidence": [
{ "text": "Uniform hedging pattern", "tactic": "STATISTICAL_LANGUAGE", "weight": 0.6 }
],
"age_calibration": { "applied": true, "age_group": "13-15", "multiplier": 1.3 },
"recommended_action": "flag_for_review",
"rationale": "Text shows uniform style and formulaic structure typical of LLM output.",
"processing_time_ms": 2225,
"language": "en",
"language_status": "stable",
"credits_used": 5
}
```
**Credits:** 5
***
## Image Detection
Analyzes uploaded images for AI-generation artifacts using a **6-signal forensic pipeline** — running vision analysis, EXIF metadata extraction, pixel statistics, C2PA Content Credentials, watermark detection, and perceptual hashing in parallel.
```
POST /api/v1/safety/synthetic-content/image
Content-Type: multipart/form-data
```
### How It Works
Send an image as `multipart/form-data`. Supported formats: **JPEG, PNG, WebP, GIF**. Max file size: **10MB**.
Six independent analysis engines run in parallel, each fault-isolated so failures in one don't affect others:
1. **Forensic vision models (ours)** — Purpose-trained models we run on our own infrastructure inspect pixel-level artifacts (face consistency, skin texture, hand anomalies, background coherence, lighting mismatches). No third-party vision API is involved.
2. **EXIF Metadata** — Checks for AI generator signatures in EXIF tags, XMP data, and PNG text chunks. Flags suspicious absence of camera metadata.
3. **Pixel Statistics** — Shannon entropy, edge density (Laplacian convolution), and channel uniformity analysis
4. **C2PA Content Credentials** — Detects and validates C2PA manifests from DALL-E, Adobe Firefly, Google Imagen, and other tools
5. **Watermark Detection** — Frequency-domain analysis for invisible watermarks (SynthID, Stable Diffusion DWT/DCT)
6. **Perceptual Hashing** — DCT-based pHash compared against a database of known synthetic content via Hamming distance
All signals are combined into a weighted ensemble: vision (30%), metadata (15%), pixel statistics (15%), C2PA provenance (15%), watermarks (10%), perceptual hash (15%). The aggregated forensic summary is fed to the classifier.
The classifier produces a final verdict. Two signals can override the classifier with definitive results:
* **C2PA declares AI generation** → forced to `confirmed_synthetic` with confidence ≥ 0.95
* **Perceptual hash matches known synthetic** → forced to `confirmed_synthetic` with confidence ≥ 0.90
### Request Fields
| Field | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------- |
| `file` | File | Yes | Image file (JPEG, PNG, WebP, GIF). Max 10MB. |
| `age_group` | string | No | Age group context (e.g., `"13-15"`) |
| `language` | string | No | ISO language code |
| `platform` | string | No | Platform context |
| `external_id` | string | No | Your internal reference ID |
| `customer_id` | string | No | Your customer/user ID |
### Response
```json theme={"dark"}
{
"endpoint": "synthetic-content",
"detected": true,
"severity": 0.9,
"level": "high",
"classification": "confirmed_synthetic",
"confidence": 0.95,
"risk_score": 0.85,
"categories": [
{ "tag": "AI_GENERATED_IMAGE", "label": "AI-generated image", "confidence": 0.95 }
],
"evidence": [
{ "text": "Uniform skin texture and symmetrical facial features", "tactic": "AI_GENERATION_ARTIFACT_DETECTION", "weight": 0.9 }
],
"recommended_action": "flag_for_review",
"rationale": "Multi-signal forensic analysis: forensic vision models detected artifacts, EXIF metadata lacks camera model, C2PA manifest confirms AI generation by DALL-E 3.",
"input_type": "image",
"vision": {
"is_likely_synthetic": true,
"synthetic_confidence": 0.95,
"artifacts": [
"unnaturally uniform skin texture on cheeks with complete absence of pores",
"perfectly symmetrical facial features",
"blurred background with smooth transitions characteristic of AI-generated depth of field"
],
"face_analysis": "Unnaturally smooth skin with no visible pores, perfectly symmetrical features.",
"overall_assessment": "Image exhibits several indicators of AI generation."
},
"metadata_analysis": {
"format": "png",
"dimensions": { "width": 1024, "height": 1024 },
"has_exif": false,
"has_camera": false,
"has_gps": false,
"ai_generator_detected": true,
"ai_generator": "DALL-E",
"suspicious_absence": true
},
"provenance": {
"has_c2pa": true,
"claim_generator": "DALL-E 3",
"is_ai_generated": true,
"ai_tool": "DALL-E 3"
},
"forensic_signals": {
"signal_count": 8,
"sources": [
{ "name": "vision", "signal_count": 3, "confidence_boost": 0.285 },
{ "name": "metadata", "signal_count": 2, "confidence_boost": 0.2 },
{ "name": "provenance", "signal_count": 1, "confidence_boost": 0.5 }
],
"combined_confidence_boost": 0.35
},
"perceptual_hash": "a3b2c1d4e5f60718",
"processing_time_ms": 3100,
"credits_used": 8
}
```
**Credits:** 8 (5 base + 3 vision)
### Forensic Signal Sources
| Signal | Weight | What It Checks |
| --------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Forensic vision models (ours)** | 30% | Face consistency, skin texture, hand anomalies, background coherence, lighting, hair rendering, text/symbols, resolution, AI model signatures |
| **EXIF Metadata** | 15% | Camera model, GPS, AI generator names in EXIF/XMP, PNG tEXt chunks (Stable Diffusion parameters), suspicious absence of camera data |
| **Pixel Statistics** | 15% | Shannon entropy, Laplacian edge density, channel uniformity — GAN images have distinctive statistical signatures |
| **C2PA Provenance** | 15% | Content Credentials manifests from DALL-E, Firefly, Imagen. **Definitive** when present — overrides classifier. |
| **Watermark** | 10% | High-frequency energy analysis, periodic pattern detection at known watermark frequencies, LSB distribution, corner entropy |
| **Perceptual Hash** | 15% | DCT-based 64-bit pHash compared against known-synthetic database. Hamming distance ≤ 10 = match. **Definitive** when matched. |
### New Response Fields
| Field | Type | Description |
| ----------------------- | ------ | ------------------------------------------------------------------------------------------------------ |
| `metadata_analysis` | object | EXIF/metadata extraction results — format, dimensions, camera presence, GPS, AI generator detection |
| `provenance` | object | C2PA Content Credentials — present only when a C2PA manifest is found |
| `forensic_signals` | object | Multi-signal ensemble summary — signal counts per source and combined confidence boost |
| `perceptual_hash` | string | 64-bit DCT-based perceptual hash of the image |
| `known_synthetic_match` | object | Present only when the perceptual hash matches a known synthetic image — includes distance and category |
***
## Audio Detection
Analyzes uploaded audio using **dual-signal forensics**: transcript-based text analysis plus spectral analysis, with the mel spectrogram assessed by our own EU-hosted vision models alongside quantitative audio statistics for synthetic speech indicators.
```
POST /api/v1/safety/synthetic-content/audio
Content-Type: multipart/form-data
```
### How It Works
Send an audio file as `multipart/form-data`. Supported formats: **MP3, WAV, M4A, OGG, FLAC, WebM**. Max file size: **25MB**.
Two analysis tracks run simultaneously:
1. **Transcription** — Audio is transcribed using Whisper (EU-hosted, GDPR-compliant)
2. **Spectral Analysis** — FFmpeg generates a mel spectrogram image and extracts audio statistics (RMS, dynamic range, silence ratio, flat factor, DC offset)
The mel spectrogram is analyzed by a dedicated forensic prompt checking for:
* Frequency band uniformity (TTS hallmark)
* Harmonic structure anomalies
* Missing breath noise and background ambience
* Onset/offset patterns, formant transitions
* Pitch contour regularity, aliasing artifacts
Transcript, spectral signals, and spectrogram analysis are combined for final classification. Even audio with no speech can be flagged if spectral analysis detects synthetic patterns.
### Request Fields
| Field | Type | Required | Description |
| ------------- | ------ | -------- | ------------------------------------------------------ |
| `file` | File | Yes | Audio file (MP3, WAV, M4A, OGG, FLAC, WebM). Max 25MB. |
| `age_group` | string | No | Age group context |
| `language` | string | No | ISO language code |
| `platform` | string | No | Platform context |
| `external_id` | string | No | Your internal reference ID |
| `customer_id` | string | No | Your customer/user ID |
### Response
```json theme={"dark"}
{
"endpoint": "synthetic-content",
"detected": true,
"severity": 0.6,
"level": "medium",
"classification": "suspected_synthetic",
"confidence": 0.7,
"risk_score": 0.5,
"categories": [
{ "tag": "AI_GENERATED_AUDIO", "label": "AI-Generated Audio", "confidence": 0.7 }
],
"recommended_action": "flag_for_review",
"rationale": "Spectral analysis shows unnaturally uniform frequency bands and missing breath noise. Transcript structure is consistent with TTS output.",
"input_type": "audio",
"transcription": {
"text": "Hello, I am calling about your account.",
"language": "en",
"duration": 3.5,
"segments": [
{ "start": 0, "end": 3.5, "text": "Hello, I am calling about your account." }
]
},
"audio_stats": {
"rms_mean": -18.5,
"rms_peak": -6.2,
"dynamic_range": 12.3,
"silence_ratio": 0.05,
"flat_factor": 0.002,
"dc_offset": 0.0001
},
"spectral_signals": [
"low_dynamic_range: 12.3 dB suggests compressed/synthetic audio",
"low_silence_ratio: 0.05 — natural speech typically has more pauses"
],
"processing_time_ms": 4200,
"language": "en",
"language_status": "stable",
"credits_used": 10
}
```
**Credits:** 7 (5 base + 2 transcription) or 10 (+ 3 for spectrogram vision analysis)
### New Response Fields
| Field | Type | Description |
| ------------------ | ------ | --------------------------------------------------------------------------------------------------- |
| `audio_stats` | object | Quantitative audio statistics — RMS mean/peak, dynamic range, silence ratio, flat factor, DC offset |
| `spectral_signals` | array | Human-readable spectral analysis indicators (e.g., low dynamic range, frequency uniformity) |
If the audio contains no intelligible speech but spectral analysis detects synthetic patterns, the endpoint returns `classification: "suspected_synthetic"` with spectral signals. If neither speech nor spectral anomalies are found, it returns `classification: "unknown"`.
***
## Video Detection
Analyzes uploaded video with a **multi-layer forensic pipeline** — per-frame vision analysis, temporal face consistency tracking, audio-visual lip-sync correlation, spectral audio forensics, and transcription.
```
POST /api/v1/safety/synthetic-content/video
Content-Type: multipart/form-data
```
### How It Works
Send a video file as `multipart/form-data`. Supported formats: **MP4, WebM, QuickTime, AVI**. Max file size: **100MB**.
Frames are extracted at even intervals using FFmpeg (default: 6 frames, max: 20). Audio is extracted as a separate track in parallel.
Five analysis tracks run simultaneously via fault-isolated `Promise.allSettled`:
1. **Per-Frame Vision** — Each frame analyzed for AI artifacts (face consistency, skin texture, background coherence) in concurrent batches of 3
2. **Temporal Consistency** — face-api.js detects faces across all frames, computes Euclidean distance between face descriptors (real \< 0.4, deepfake > 0.6), and measures landmark stability via eye-to-nose ratio variance
3. **Lip-Sync Correlation** — Mouth openness (from 68-point face landmarks) correlated against frame-aligned audio energy (via FFmpeg). Pearson correlation > 0.5 = real speech, \< 0.3 = lip-sync deepfake
4. **Spectral Audio Analysis** — Mel spectrogram + audio statistics (same as audio endpoint)
5. **Transcription** — Whisper transcription of the audio track
All signals are aggregated via weighted ensemble and fed to the classifier. Temporal anomalies and lip-sync mismatches provide strong deepfake indicators that single-frame analysis cannot detect.
### Request Fields
| Field | Type | Required | Description |
| ------------- | ------ | -------- | -------------------------------------------------- |
| `file` | File | Yes | Video file (MP4, WebM, QuickTime, AVI). Max 100MB. |
| `max_frames` | number | No | Frames to extract (default: 6, max: 20) |
| `age_group` | string | No | Age group context |
| `language` | string | No | ISO language code |
| `platform` | string | No | Platform context |
| `external_id` | string | No | Your internal reference ID |
| `customer_id` | string | No | Your customer/user ID |
### Response
```json theme={"dark"}
{
"endpoint": "synthetic-content",
"detected": true,
"severity": 0.7,
"level": "high",
"classification": "suspected_synthetic",
"confidence": 0.8,
"risk_score": 0.65,
"categories": [
{ "tag": "AI_MANIPULATED_MEDIA", "label": "AI-Manipulated Media", "confidence": 0.8 }
],
"recommended_action": "escalate",
"rationale": "Temporal analysis detected face identity drift across 3 frame pairs. Lip-sync correlation (0.21) is well below the 0.3 threshold for authentic speech.",
"input_type": "video",
"video": {
"duration_seconds": 12.5,
"frames_analyzed": 6,
"has_audio": true
},
"temporal_consistency": {
"frames_with_faces": 6,
"total_frames": 6,
"identity_consistency_score": 0.42,
"landmark_stability_score": 0.65,
"temporal_consistency_score": 0.51,
"anomalous_frame_pairs": [
{ "frame_a": 1, "frame_b": 2, "distance": 0.72 },
{ "frame_a": 3, "frame_b": 4, "distance": 0.68 }
],
"signals": [
"identity_drift: 2 frame pairs show face identity changes (max distance: 0.720)",
"low_identity_consistency: face identity varies significantly across frames (score: 0.42)"
]
},
"lip_sync": {
"correlation": 0.21,
"has_silent_mouth_movement": false,
"has_voice_without_movement": true,
"signals": [
"poor_lip_sync: mouth-audio correlation is 0.21 (threshold: 0.3)",
"voice_without_movement: audio present in 4/6 frames without mouth movement"
]
},
"transcription": {
"text": "Speech content from the video.",
"language": "en",
"duration": 12.5
},
"audio_stats": {
"rms_mean": -20.1,
"dynamic_range": 15.8,
"silence_ratio": 0.12,
"flat_factor": 0.001
},
"processing_time_ms": 15000,
"language": "en",
"credits_used": 25
}
```
**Credits:** 5 base + 3 per frame + 2 if audio present
For example, 6 frames with audio = 5 + 18 + 2 = **25 credits**.
### New Response Fields
| Field | Type | Description |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `temporal_consistency` | object | Face identity tracking across frames — consistency scores, landmark stability, anomalous frame pairs |
| `lip_sync` | object | Audio-visual lip-sync correlation — Pearson coefficient, silent mouth movement detection, voice-without-movement detection |
| `audio_stats` | object | Quantitative audio statistics from spectral analysis |
| `spectral_signals` | array | Spectral analysis indicators (when detected) |
### Temporal Consistency Signals
| Signal | Meaning |
| -------------------------- | -------------------------------------------------------------------------------------------- |
| `identity_drift` | Face descriptors differ significantly between consecutive frames — classic deepfake artifact |
| `low_identity_consistency` | Average face identity distance is high across all frames |
| `unstable_landmarks` | Facial geometry (eye-to-nose ratio) varies abnormally between frames |
| `consistent_face` | Face identity and geometry are stable — reduces synthetic confidence |
### Lip-Sync Signals
| Signal | Meaning |
| ------------------------ | ------------------------------------------------------------------------ |
| `poor_lip_sync` | Mouth movement and audio energy have low correlation (\< 0.3) |
| `good_lip_sync` | Strong correlation (> 0.5) between mouth and audio — authentic indicator |
| `silent_mouth_movement` | Mouth opens in > 30% of frames without corresponding audio |
| `voice_without_movement` | Audio present in > 30% of frames without mouth movement |
If the video has no audio track, the `transcription`, `lip_sync`, `audio_stats`, and `spectral_signals` fields are omitted and no transcription credits are charged.
***
## Credit Summary
| Endpoint | Path | Credits |
| -------- | --------------------------------- | ----------------------- |
| Text | `/safety/synthetic-content` | 2 |
| Image | `/safety/synthetic-content/image` | 5 |
| Audio | `/safety/synthetic-content/audio` | 4-7 |
| Video | `/safety/synthetic-content/video` | 2 + 3/frame + 2 (audio) |
## Multi-Endpoint Support
The text-based synthetic content detector is available in the [Multi-Endpoint](/api-reference/introduction) fan-out. Include `"synthetic-content"` in your endpoint list:
```json theme={"dark"}
{
"text": "content to analyze",
"endpoints": ["bullying", "grooming", "synthetic-content"]
}
```
Image, audio, and video synthetic detection are multipart-only and not available through the multi-endpoint batch.
# Trust Center
Source: https://docs.tuteliq.ai/trust
Security, compliance, and transparency at Tuteliq
## Trust starts with transparency
The **Tuteliq Trust Center** at [trust.tuteliq.ai](https://trust.tuteliq.ai) is where you can verify our security posture, compliance status, and data handling practices — all in one place.
View real-time security and compliance information at **trust.tuteliq.ai**
## What you'll find
Infrastructure security, encryption standards, access controls, and vulnerability management policies.
Current compliance posture for GDPR, KOSA, EU Digital Services Act, UK Online Safety Act, and more.
Full list of third-party sub-processors, their locations, purposes, and DPA links.
Retention policies, data minimization practices, and encryption-at-rest and in-transit details.
## Key commitments
| Area | Commitment |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Encryption** | TLS 1.3 in transit, AES-256 at rest |
| **Data residency** | EU by default; US region available on request |
| **Retention** | No user content, ever. The only record kept is the incident report, which is the model's response and contains no content: encrypted at rest by default, or end-to-end encrypted if you register a key. Opt out entirely with `incident_moderation_enabled: false`. |
| **Erasure** | Full data deletion within 1 hour of request |
| **Access control** | Role-based access, audit logging, API key scoping |
| **Incident response** | 24-hour notification for security incidents |
## What we keep, what we don't
We operate a **content-out, metadata-in** pipeline. The distinction matters because everything we say about your data flows from it.
### What we never store
| Data type | Lifetime in our infra |
| ----------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Messages / chat text | Discarded after analysis completes |
| Images / video frames | Discarded after analysis completes |
| Audio / voice samples | Discarded after transcription + analysis |
| Documents (PDFs) | Processed in memory only |
| Biometric inputs (selfies, document images, face landmarks) | Discarded; no hashes, no derivatives |
| Conversation history across calls | Replaced by signed, customer-held [continuation tokens](/continuation-tokens) |
### What we do store
One thing: the **incident report**. It is the model's response, not your content. It exists so the moderation dashboard has something to show you, which is why it is **entirely optional**: set `incident_moderation_enabled: false` on the account or on any single call and nothing is written at all.
| Data | Why | Retention |
| -------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------ |
| Classification result (category, severity, confidence, recommended action) | Powers your moderation dashboard | 90 days, or never if disabled |
| Model rationale explaining the flag, stripped of quoted content and PII before it is written | Analysts need context to review an incident | 90 days, encrypted at rest, or never if disabled |
| Aggregated trend signals (k-anonymised) | Powers `/intelligence` endpoints | 90 days |
| API logs (no message body) | Operations, billing, abuse | 30 days |
**Two encryption levels, and the stronger one is yours to switch on.** By default the incident record is encrypted at rest with server-side AES-256, which Tuteliq holds the key for. If you [register a public key](#end-to-end-encryption-for-stored-rationales-opt-in), incident fields are instead wrapped end-to-end so that **only you can decrypt them and Tuteliq cannot** — a cryptographic guarantee rather than a policy promise.
In both cases the underlying content was already discarded, so what is being protected is the model's assessment, never your users' messages. And if you would rather nothing were written at all, `incident_moderation_enabled: false` does exactly that.
### Your controls
* **Full deletion** within 1 hour of a `DELETE /account/data` request
* **Encryption at rest** (AES-256) for all stored metadata
* **End-to-end encryption (opt-in)** — register an RSA public key and Tuteliq can no longer decrypt your stored rationales (see below)
* **EU data residency** by default; US region on request
* **No cross-customer linking** — deployer fingerprints scoped per request
The short version: **your users' content lives in your platform, not ours. Tuteliq holds the analysis outputs your team needs to review — nothing else.**
### End-to-end encryption for stored rationales (opt-in)
By default the metadata fields we retain — LLM rationale, visual description, source data — are encrypted at rest with a Tuteliq-held AES-256 key, then decrypted server-side when your dashboard requests them. This is fast and requires no setup on your side.
For customers who want stronger separation, we support **customer-managed end-to-end encryption**: you generate an RSA keypair, register the public key with Tuteliq, and we use it to wrap every new incident's metadata. From that point on we cannot decrypt those fields — only your dashboard, holding the matching private key, can.
| Property | Default (server-side) | E2E (opt-in) |
| ---------------------------- | ----------------------------- | ------------------------------------------------------- |
| Encryption scheme | AES-256-GCM | RSA-OAEP-2048/4096 + AES-256-GCM (hybrid) |
| Decryption key holder | Tuteliq | You |
| Tuteliq can read rationale | Yes (for dashboard rendering) | **No** |
| Private-key recovery if lost | n/a | **Your responsibility** — lost key means lost rationale |
| Activation | None — default behaviour | `POST /api/v1/account/encryption-key` |
| Rollback / rotation | n/a | `POST` a new key any time; `DELETE` to revoke |
The scheme identifier (`TLQ-HYBRID-RSA-OAEP-AES-256-GCM-v1`) and key fingerprint are embedded in every encrypted record so you can verify integrity client-side.
This is **opt-in by design** because losing the private key permanently strands every record encrypted under it — Tuteliq cannot recover them. Teams that don't want that operational burden should stay on the default server-side AES at rest. Teams with stricter compliance requirements (or who want a verifiable cryptographic guarantee that Tuteliq cannot read their rationales) should register a key.
Existing incidents written before you register a key remain readable under the server-side scheme; only new incidents use the hybrid scheme.
## Compliance documentation
The following endpoints are publicly accessible and require no authentication:
| Endpoint | Description |
| -------------------------------- | ----------------------------------------------- |
| `GET /compliance/dpa` | Current Data Processing Agreement (PDF) |
| `GET /compliance/sub-processors` | List of sub-processors with locations and roles |
| `GET /compliance/retention` | Data retention policy by data type |
See the [GDPR Compliance](/gdpr) page for detailed data subject rights endpoints and consent management.
## Independent penetration test
Tuteliq's API was penetration tested by an independent third party (Workstreet)
in February 2026, against the production surface at `api.tuteliq.ai`.
| | Result |
| ------------------- | -------------------------------------------- |
| Overall risk rating | **Low** |
| Critical findings | **0** |
| High findings | **0** |
| Medium findings | **0** |
| Low findings | 2, both remediated |
| Informational | 2, one remediated and one accepted by design |
Both low-severity findings were closed within two weeks and the fixes verified in
production. The one informational finding we did not action was the public
exposure of the OpenAPI specification, which is intentional: it is the
documentation endpoint this site is built from.
The full report is available under NDA. Contact
[security@tuteliq.ai](mailto:security@tuteliq.ai).
## Certification
Tuteliq also offers a **free certification program** for individuals and organizations who want to demonstrate their commitment to child safety. See the [Certification](/certification) page for details, or start directly at [tuteliq.ai/certify](https://tuteliq.ai/certify).
## Questions
For security inquiries, compliance questions, or to request the [penetration test report](#independent-penetration-test) under NDA, contact [security@tuteliq.ai](mailto:security@tuteliq.ai). Vulnerability reports are welcome at the same address; see [/.well-known/security.txt](https://api.tuteliq.ai/.well-known/security.txt).
# Age & Identity Verification
Source: https://docs.tuteliq.ai/verification
Production-grade age and identity verification with document intelligence, biometric matching, liveness detection, and multi-layer fraud prevention — covering 45 countries
Tuteliq Verification is a production-grade identity and age verification pipeline that combines document intelligence, biometric matching, liveness detection, and multi-layer fraud prevention in a single API call.
Most verification providers give you OCR and a face match. Tuteliq cross-references **every data source on the document against every other** — MRZ check digits, barcode data, OCR text, front vs. back, document vs. selfie — and flags any inconsistency as potential tampering. This catches forgeries that pass single-layer checks.
**Recommended integration path for mobile, web, and native apps.** Create a session on your server, open the URL in a webview or QR code, get the result back via Server-Sent Events. Includes code samples for iOS (Swift), Android (Kotlin), React Native, Flutter, and web (iframe).
Confirm user age via document analysis, biometric estimation, or both. Direct-submission API for same-device flows.
Full identity confirmation with document authentication, face matching, liveness detection, and fraud prevention.
45 countries with algorithmic validation. ICAO 9303 MRZ. PDF417 barcode decoding.
7 cross-referencing layers, deterministic document validation, and IP geolocation checks.
## What makes Tuteliq different
Algorithmic check digit validation for CPF, personnummer, Aadhaar, Codice Fiscale, CURP, SSN, and 39 more. Not just format checks — full mathematical verification.
MRZ vs. OCR. Barcode vs. OCR. Front vs. back. Document vs. selfie. Declared type vs. detected type. IP vs. document country. OCR confidence gating. Every inconsistency is flagged.
Document validation is deterministic: ICAO 9303 check digits, PDF417/AAMVA decoding, and field cross-referencing. Age comes from our own model on our own infrastructure. No identity document or selfie is sent to a language model.
## Tier availability
| Feature | Minimum Tier | Credits |
| -------------------------------- | ----------------- | ------------------- |
| Age Verification (liveness only) | Assure (\$499/mo) | 10 per verification |
| Age Verification (full) | Assure (\$499/mo) | 20 per verification |
| Identity Verification | Assure (\$499/mo) | 25 per verification |
***
## Age Verification
`POST /api/v1/verify/age/submit`
Verify a user's age through document analysis, biometric age estimation, or both. Returns a verified age range, confidence score, and detailed document intelligence.
### Verification methods
| Method | How it works | Best for |
| ------------- | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| **Document** | Extracts DOB from government ID via OCR, MRZ parsing, and barcode decoding | High-assurance age gates, parental consent |
| **Biometric** | Estimates age from a selfie using AgeNet, our own model, run on our infrastructure (child/teen/adult classification) | Frictionless age checks, onboarding flows |
| **Combined** | Document + biometric with cross-reference — flags age inconsistencies > 10 years | Maximum assurance — verifies the document belongs to the person |
### Request
```typescript Node.js theme={"dark"}
const result = await tuteliq.verifyAge({
document: fs.createReadStream('id-front.jpg'), // Government-issued ID
documentBack: fs.createReadStream('id-back.jpg'), // Optional — enables barcode reading + back cross-ref
selfie: fs.createReadStream('selfie.jpg'), // Optional — for biometric estimation
method: 'combined', // 'document' | 'biometric' | 'combined'
});
console.log(result.verified); // true
console.log(result.estimated_age); // 15
console.log(result.age_range); // "13-15"
console.log(result.is_minor); // true
console.log(result.confidence); // 0.97
console.log(result.document_type); // "passport"
```
```python Python theme={"dark"}
result = client.verify_age(
document=open("id-front.jpg", "rb"),
document_back=open("id-back.jpg", "rb"), # Optional
selfie=open("selfie.jpg", "rb"),
method="combined",
)
print(result.verified) # True
print(result.estimated_age) # 15
print(result.age_range) # "13-15"
print(result.is_minor) # True
```
```bash cURL theme={"dark"}
curl -X POST https://api.tuteliq.ai/api/v1/verify/age/submit \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "document=@id-front.jpg" \
-F "document_back=@id-back.jpg" \
-F "selfie=@selfie.jpg" \
-F "method=combined"
```
### Response
```json theme={"dark"}
{
"verified": true,
"estimated_age": 15,
"age_range": "13-15",
"is_minor": true,
"confidence": 0.97,
"method": "combined",
"document_type": "passport",
"document_country": "GB",
"biometric_age": 15,
"document_age": 15,
"document": {
"ocr_confidence": 94,
"mrz_valid": true,
"document_number_valid": true,
"expired": false
},
"credits_used": 20
}
```
### Response fields
| Field | Type | Description |
| -------------------------------- | ------- | ------------------------------------------------------------- |
| `verified` | boolean | Whether age verification succeeded |
| `estimated_age` | integer | Best estimate of user's age |
| `age_range` | string | Tuteliq age bracket: `under-10`, `10-12`, `13-15`, or `14-17` |
| `is_minor` | boolean | Whether the user is under 18 |
| `confidence` | float | Confidence score (0.0-1.0) |
| `method` | string | Method used: `document`, `biometric`, or `combined` |
| `document_type` | string | Detected document type |
| `document_country` | string | ISO 3166-1 alpha-2 country code |
| `biometric_age` | integer | Age estimated from selfie (if provided) |
| `document_age` | integer | Age from document DOB (if provided) |
| `document.ocr_confidence` | integer | OCR confidence percentage (0-100) |
| `document.mrz_valid` | boolean | Whether MRZ check digits passed (if MRZ present) |
| `document.document_number_valid` | boolean | Whether document number passed algorithmic validation |
| `document.expired` | boolean | Whether the document has expired |
| `credits_used` | integer | Credits consumed |
### Age extraction sources
DOB is extracted from multiple sources and cross-referenced. Priority order:
1. **MRZ** (Machine Readable Zone) — Most reliable. ICAO 9303 check digit validated.
2. **PDF417 barcode** — US/Canadian driver's licenses. AAMVA-encoded structured data.
3. **OCR labels** — Text patterns like "Date of Birth:", "DOB:", date formats.
4. **Selfie estimation** — AgeNet, our own convolutional age-estimation model, run on our infrastructure (fallback). No LLM and no third-party vision API touches a verification image.
When multiple sources disagree, the verification flags the inconsistency.
### Supported documents
| Document Type | Coverage |
| ---------------- | ------------------------------------------------------------------------------------ |
| Passport | All ICAO-compliant passports worldwide (MRZ validated) |
| National ID card | All countries with MRZ-equipped cards + 45 countries with document number validation |
| Driver's license | All countries via OCR + US/Canada via PDF417 barcode |
| Residence permit | Via OCR text extraction |
Documents must be a clear, well-lit photo in JPEG or PNG format. Maximum file size: 10MB.
***
## Identity Verification
`POST /api/v1/verify/identity/submit`
Full KYC identity verification combining document authentication, face matching, liveness detection, and country-specific national-ID validation. Assure plan and above.
### What it checks
Every identity verification runs **all** of these checks automatically:
| Check | What it does |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Document OCR** | Multilingual text extraction (Tesseract sidecar with multi-pass MRZ-tolerance pipeline) |
| **MRZ validation** | ICAO 9303 TD1/TD2/TD3 check-digit validation via `mrz` + `mrz-fast` libraries |
| **Document number validation** | Algorithmic check-digit verification for 45 countries (personnummer, CPF, Aadhaar, CURP, BSN, NIF, NINO, etc.) |
| **Face detection + matching** | `@vladmandic/face-api` model in dedicated sidecar; vector-distance match between document portrait and selfie |
| **Liveness** | Multi-signal: landmark motion, blink, texture/moiré, depth cues, cross-frame consistency, single-use cryptographic challenge token |
| **Synthetic / deepfake detection** | CLIP-based classifier (`deep-image-analysis` sidecar) on the selfie |
| **Document expiry** | Checks expiry from labels and MRZ |
| **Country resolution** | Inferred from MRZ issuing state, OCR text, or explicit `country_code` parameter |
### Request
```bash cURL theme={"dark"}
curl -X POST https://api.tuteliq.ai/api/v1/verify/identity/submit \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "id_front=@id-front.jpg" \
-F "id_back=@id-back.jpg" \
-F "selfie=@selfie.jpg" \
-F "liveness_token=YOUR_LIVENESS_TOKEN" \
-F "document_type=passport"
```
Pass `passport=@passport.jpg` instead of `id_front`/`id_back` when verifying a passport. The `liveness_token` is issued by the liveness challenge step (see `Liveness detection` doc).
### Response
```json theme={"dark"}
{
"verification_id": "string",
"status": "verified",
"full_name": "string",
"first_name": "string",
"last_name": "string",
"date_of_birth": "string (ISO 8601)",
"document_type": "passport",
"country_code": "SWE",
"country_name": "Sweden",
"document_number": "string",
"document_number_valid": true,
"sex": "M",
"nationality": "string",
"place_of_birth": "string",
"height_cm": 0,
"issuing_authority": "string",
"issue_date": "string (ISO 8601)",
"expiration_date": "string (ISO 8601)",
"national_id": {
"value": "string",
"valid": true,
"type": "personnummer",
"country_code": "SWE"
},
"mrz_valid": true,
"liveness": { "valid": true, "reason": "string" },
"face_match": { "matched": true, "distance": 0.0, "confidence": 0.0 },
"credits_used": 0
}
```
All identity fields are `null` when not extractable (e.g. `height_cm` for national ID cards that don't print height, or `national_id` for countries without a per-country extractor in this release).
### Response fields
| Field | Type | Description |
| ----------------------- | --------------- | ------------------------------------------------------------------ |
| `verification_id` | string \| null | Persistent ID for the record |
| `status` | string | `verified` \| `failed` \| `needs_review` |
| `full_name` | string \| null | Combined name as extracted from the document |
| `first_name` | string \| null | Given names — prefers MRZ-parsed, falls back to heuristic split |
| `last_name` | string \| null | Surname — same source preference |
| `date_of_birth` | string \| null | ISO 8601 |
| `document_type` | string \| null | `passport` \| `national_id` \| `drivers_license` |
| `country_code` | string \| null | ISO 3166-1 alpha-3 |
| `country_name` | string \| null | English country name |
| `document_number` | string \| null | Document number as printed / MRZ-parsed |
| `document_number_valid` | boolean \| null | Country-specific algorithmic validation passed |
| `sex` | string \| null | `M` \| `F` |
| `nationality` | string \| null | Text-extracted; MRZ fallback when available |
| `place_of_birth` | string \| null | As printed on the document |
| `height_cm` | integer \| null | Height in cm (range 50–250) when printed on the document |
| `issuing_authority` | string \| null | Issuing body (e.g. `POLISMYNDIGHETEN` for Swedish passports) |
| `issue_date` | string \| null | ISO 8601, when printed |
| `expiration_date` | string \| null | ISO 8601 |
| `national_id` | object \| null | Country-specific personal/national ID (see below) |
| `mrz_valid` | boolean \| null | ICAO 9303 check digits all passed (null if no MRZ on the document) |
| `liveness` | object | `{ valid: boolean, reason?: string }` |
| `face_match` | object \| null | `{ matched: boolean, distance: number, confidence: number }` |
| `credits_used` | integer | Credits consumed |
### `national_id` object
Returned only when the document's country has a per-country extractor in this release.
| Field | Type | Description |
| -------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `value` | string | The ID number as printed (with separator preserved when present) |
| `valid` | boolean | Country-specific algorithmic validation passed |
| `type` | string | Canonical name — `personnummer` (SWE), `fødselsnummer` (NOR), `henkilötunnus` (FIN), `DNI/NIE` (ESP), `CPF` (BRA) |
| `country_code` | string | ISO 3166-1 alpha-3 |
Countries with validators but no extractor yet (returns `null` for `national_id` until extractors land): DEU, FRA, ITA, NLD, GBR, IRL, CHE, AUT, CZE, ROU, HRV, BGR, GRC, HUN, EST, LVA, LTU, ARG, MEX, CHL, COL, PER, ECU, URY, CAN, USA, JPN, KOR, ZAF, THA, TUR.
### Verification outcomes
| Status | Meaning | Examples |
| -------------- | --------------------------------------- | ----------------------------------------------------------------------- |
| `verified` | All checks passed | Valid document, face match, liveness passed |
| `failed` | Hard failure — definitive fraud signal | Liveness failed, face mismatch, document expired |
| `needs_review` | Soft failure — human review recommended | Low OCR confidence, low face-match confidence, partial field extraction |
***
## Integration patterns
### Age gate on sign-up
```typescript theme={"dark"}
// 1. Verify age during sign-up
const verification = await tuteliq.verifyAge({
selfie: selfieStream,
method: 'biometric',
});
// 2. Store the verified age group
const ageGroup = verification.age_range; // "13-15"
// 3. Use verified age group in all safety calls
const safety = await tuteliq.detectUnsafe({
text: messageContent,
ageGroup: ageGroup, // Properly calibrated risk scoring
});
```
### Parental consent flow (COPPA)
```typescript theme={"dark"}
// 1. Verify child's age
const childAge = await tuteliq.verifyAge({
selfie: childSelfieStream,
method: 'biometric',
});
if (childAge.estimated_age < 13) {
// 2. Verify parent's identity
const parent = await tuteliq.verifyIdentity({
document: parentIdStream,
selfie: parentSelfieStream,
});
if (parent.verified && !parent.is_minor) {
await grantParentalConsent(childUserId, parent);
}
}
```
### Moderator/admin verification
```typescript theme={"dark"}
const identity = await tuteliq.verifyIdentity({
document: documentStream,
selfie: selfieStream,
});
if (identity.verified && !identity.is_minor) {
await grantModeratorRole(userId);
}
```
## Error handling
Verification errors use the `VERIFY_11xxx` code range:
| Code | HTTP | Description |
| --------------------------------------------- | ---- | ---------------------------------------------------------------------------------------- |
| `VERIFY_11001` `VERIFY_OCR_FAILED` | 500 | OCR could not extract text from the document — retake under better lighting / less glare |
| `VERIFY_11002` `VERIFY_NO_DOB_FOUND` | 400 | Date of birth could not be extracted — try a clearer page / different document |
| `VERIFY_11003` `VERIFY_FACE_NOT_DETECTED` | 400 | No face detected in the selfie or document portrait |
| `VERIFY_11004` `VERIFY_FACE_MISMATCH` | 400 | Selfie does not match the document portrait |
| `VERIFY_11005` `VERIFY_LIVENESS_FAILED` | 400 | Liveness check failed — possible spoofing or stale capture |
| `VERIFY_11006` `VERIFY_SESSION_NOT_FOUND` | 404 | Session ID unknown — recreate the session |
| `VERIFY_11007` `VERIFY_SESSION_EXPIRED` | 410 | Session past TTL — recreate the session |
| `VERIFY_11008` `VERIFY_SESSION_INVALID_TOKEN` | 401 | Session token invalid — recreate the session, do not replay |
| `VERIFY_11009` `VERIFY_SESSION_ALREADY_USED` | 409 | Session token already consumed |
| `VERIFY_11010` `VERIFY_SESSION_WRONG_STATE` | 409 | Session not in the right state for this call |
| `VERIFY_11011` `VERIFY_NOT_ENABLED` | 403 | Verification not provisioned for this account |
| `VERIFY_11012` `VERIFY_IDENTITY_NOT_ENABLED` | 403 | Identity mode requires the Assure plan or above |
| `VERIFY_11013` `VERIFY_NO_COUNTRY_FOUND` | 400 | Country could not be inferred — pass `country_code` explicitly |
```bash cURL example error response theme={"dark"}
{
"error": {
"code": "VERIFY_11005",
"message": "Liveness check failed",
"request_id": "req_...",
"retryable": false,
"status_url": "https://tuteliq.ai/status",
"documentation_url": "https://tuteliq.ai/docs/errors#VERIFY_11005"
}
}
```
## Data handling
Verification involves sensitive personal data. Tuteliq processes documents and selfies in real time and does **not** store images or extracted PII after the verification is complete. Only the verification result (age range, pass/fail) is retained. See the [GDPR](/gdpr) page for data handling details.
## Next steps
Deep dive into 45-country document validation, MRZ parsing, and barcode reading.
How visual liveness analysis prevents spoofing attacks.
Multi-layer cross-referencing, MRZ check-digit validation, and geographic consistency checks.
# Cross-device verification flow
Source: https://docs.tuteliq.ai/verification/cross-device-flow
End-to-end integration guide for embedding Tuteliq verification in your app via a webview, mobile redirect, or QR-code handoff. Covers session creation, live status updates, webview hosting, and result retrieval.
The session-based flow is the recommended integration path when:
* Your app runs on a device that may not have a camera (desktop web app, kiosk)
* You want the verification UI to live on the user's mobile device while the rest of your app runs elsewhere
* You want to embed Tuteliq's verified UI in a webview without rebuilding the capture flow yourself
You create a session on your server, hand the user a URL or QR code that opens Tuteliq's hosted capture page (or embed that page in a webview), and your server gets the result back via Server-Sent Events or polling. No image data passes through your servers; Tuteliq processes the documents and selfie directly from the mobile device.
## Age Verification Quick Start
Verify a user's age in three steps with a hosted verification session. This is the exact flow you can try interactively in the API Playground in your dashboard, the document scan, selfie, and liveness checks all happen on Tuteliq's hosted page, so you never handle the user's ID or biometric data yourself.
Hosted age verification requires the Assure plan or above and costs 10 credits for liveness-only or 20 credits for a full document verification. You'll need an API key, create one under **API Keys** in your dashboard.
### How it works
Tuteliq creates a short-lived, hosted verification session and returns a URL. You send your user to that URL, they complete the checks on Tuteliq's page, and you poll the session until it reaches a terminal status.
1. Create a session with `mode: "age"`.
2. Redirect the user to the returned verification URL.
3. Poll the session for the result.
### 1. Create a session
Send a `POST` to `/api/v1/verify/session` with the mode set to `age`.
```bash theme={"dark"}
curl -X POST https://api.tuteliq.ai/api/v1/verify/session \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "mode": "age" }'
```
The response contains the session ID, the hosted verification URL, and an expiry timestamp (Unix milliseconds):
```json theme={"dark"}
{
"session_id": "string",
"mobile_url": "string (URL with single-use token)",
"expires_at": 0,
"mode": "string (\"age\" or \"identity\")"
}
```
### 2. Send the user to verify
Open the `mobile_url` in a new tab, redirect to it, or surface it as a QR code / deep link. The user scans their ID document and completes a selfie liveness check on Tuteliq's hosted page.
### 3. Poll for the result
Poll `GET /api/v1/verify/session/{session_id}` every few seconds until the top-level `status` reaches a terminal value: `completed`, `failed`, `expired`, or `cancelled`. The verification outcome lives at `result.status`.
```bash theme={"dark"}
curl https://api.tuteliq.ai/api/v1/verify/session/{session_id} \
-H "Authorization: Bearer YOUR_API_KEY"
```
A completed, successful age-mode session has this shape:
```json theme={"dark"}
{
"session_id": "string",
"status": "string (one of: pending, in_progress, completed, failed, expired, cancelled)",
"result": {
"status": "string (one of: verified, failed, needs_review)",
"age": 0,
"date_of_birth": "string (ISO 8601 date)",
"is_minor": false,
"document": {
"ocr_confidence": 0,
"name_extracted": "string",
"dob_extracted": "string (ISO 8601 date)",
"document_number": "string",
"document_number_valid": true,
"sex": "string (M, F, X)",
"country_code": "string (ISO 3166-1 alpha-3)",
"document_type": "string (passport, id_card, drivers_license, residence_permit)",
"expiration_date": "string (ISO 8601 date)",
"expired": false,
"mrz_valid": null,
"mrz_fields": null
},
"barcode": null,
"document_validation": {
"age_document_consistent": true,
"age_discrepancy_years": 0,
"age_at_issue": 0,
"issue_date": "string (ISO 8601 date)",
"validity_period_days": 0,
"validity_period_normal": true,
"checks": [
{ "check": "string", "passed": true, "detail": "string" }
]
},
"face_match": {
"matched": true,
"distance": 0.0,
"confidence": 0.0
},
"liveness": {
"valid": true,
"visual_score": 0.0,
"visual_checks": {
"landmark_motion": true,
"texture": true,
"depth_cues": true,
"cross_frame": true,
"blink": true,
"skin": true
}
},
"failure_reasons": [],
"warnings": []
},
"created_at": 0,
"expires_at": 0
}
```
The fields most integrations care about are `result.status` (`verified`, `failed`, or `needs_review`), `result.is_minor` (boolean), `result.age` (integer years), and `result.failure_reasons` (array, empty on success). The full `result.document`, `result.document_validation`, `result.face_match`, and `result.liveness` sub-objects are available when you need detailed evidence for your own audit logs or moderator UI.
### Full example
The complete create → redirect → poll flow in TypeScript:
```typescript theme={"dark"}
// 1. Create a hosted age verification session
const create = await fetch("https://api.tuteliq.ai/api/v1/verify/session", {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ mode: "age" }),
});
const { session_id, mobile_url } = await create.json();
// 2. Send the user to the hosted verification page
// (open in a new tab, redirect, or render as a mobile deep link)
window.open(mobile_url, "_blank");
// 3. Poll for the result until the top-level status is terminal
async function pollSession(id: string) {
const terminalStates = ["completed", "failed", "expired", "cancelled"];
for (let attempt = 0; attempt < 60; attempt++) {
const res = await fetch(
`https://api.tuteliq.ai/api/v1/verify/session/${id}`,
{ headers: { Authorization: `Bearer ${API_KEY}` } },
);
const data = await res.json();
const status = String(data.status || "").toLowerCase();
if (terminalStates.includes(status)) return data; // session is done
await new Promise((r) => setTimeout(r, 3000)); // wait 3s, then retry
}
throw new Error("Verification session timed out after 3 minutes.");
}
const session = await pollSession(session_id);
const verification = session.result;
if (
session.status === "completed" &&
verification?.status === "verified" &&
verification?.is_minor === false
) {
// grant access; user is age-verified adult
console.log(`Verified age ${verification.age}`);
}
```
The rest of this page covers the deeper integration patterns: live updates via Server-Sent Events instead of polling, embedding the verification UI in a mobile webview, and handling identity verification (full KYC, Assure plan and above).
## Flow at a glance
```
┌──────────────────┐ 1. POST /api/v1/verify/session
│ Your server │ ──────────────────────────────────────► ┌─────────────────┐
│ (Node, Python, │ │ Tuteliq API │
│ Go, anything) │ ◄────────────────────────────────────── │ (europe-west1) │
└──────────────────┘ {session_id, mobile_url, expires_at} └─────────────────┘
│
│ 2. Hand mobile_url to user
│ (webview, QR, redirect)
▼
┌──────────────────┐ 3. User completes capture on device
│ User's mobile │ ──────────────────────────────────────► ┌─────────────────┐
│ /age/?session=…│ │ Tuteliq capture │
│ /identity/?… │ │ UI (hosted by │
└──────────────────┘ │ Tuteliq) │
└────────┬────────┘
│
┌────────────────────────────────────────────────────────────────┘
│ 4a. Live status pushed via SSE
│ OR
│ 4b. Poll GET /api/v1/verify/session/:id
▼
┌──────────────────┐
│ Your server │ → display result, gate access, etc.
└──────────────────┘
```
## Step 1: Create a session (server-side)
Always create the session on your server with your API key. **Never expose your API key on the client.**
```typescript Node.js theme={"dark"}
import { Tuteliq } from "@tuteliq/sdk";
const tuteliq = new Tuteliq({ apiKey: process.env.TUTELIQ_API_KEY });
const session = await tuteliq.createVerificationSession({
mode: "age", // "age" or "identity"
document_type: "passport", // optional hint
external_id: "user-12345", // optional; lets you correlate to your own user
redirect_url: "https://yourapp.com/verify/done", // optional; user returns here after completion
});
console.log(session.session_id); // "sess_a1b2c3d4..."
console.log(session.url); // mobile-ready URL with one-time token
console.log(session.expires_at); // ISO 8601, typically 15 minutes from creation
```
```python Python theme={"dark"}
from tuteliq import Tuteliq
client = Tuteliq(api_key=os.environ["TUTELIQ_API_KEY"])
session = client.create_verification_session(
mode="age", # "age" or "identity"
document_type="passport", # optional
external_id="user-12345", # optional
redirect_url="https://yourapp.com/verify/done", # optional
)
print(session.session_id)
print(session.mobile_url)
print(session.expires_at)
```
```bash cURL theme={"dark"}
curl -X POST https://api.tuteliq.ai/api/v1/verify/session \
-H "Authorization: Bearer $TUTELIQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "age",
"document_type": "passport",
"external_id": "user-12345"
}'
```
### Response
```json theme={"dark"}
{
"session_id": "string",
"mobile_url": "string (URL with single-use token)",
"expires_at": 0,
"mode": "string (\"age\" or \"identity\")"
}
```
`expires_at` is a Unix millisecond timestamp; sessions are valid for 15 minutes after creation. The `mobile_url` is single-use and tied to the session. Tokens are revoked the moment the session completes, fails, or expires.
### Modes and tier availability
| Mode | Returns | Minimum tier |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `age` | Verified age, `is_minor` boolean, document OCR fields, document validation checks, face match, liveness. Returned to your server so you can drive your age gate, then destroyed on Tuteliq's side. | Assure |
| `identity` | Full KYC payload: name, date of birth, document type, country, document validity, face match score, liveness. | Assure |
Both modes return the verified data to **your** server so you can act on it. Tuteliq does not retain any of it after the response is sent. See [Zero retention](#zero-retention) below.
## Step 2: Hand the URL to the user
Three patterns, pick the one that fits your product:
### Pattern A: Embed in a webview (same-device flow)
The user's session lives inside your app. The webview loads `mobile_url`, the user completes capture without leaving your app, you get the result back via SSE.
This is the most common pattern for mobile apps.
```swift theme={"dark"}
import WebKit
class VerificationViewController: UIViewController, WKNavigationDelegate {
private var webView: WKWebView!
private let sessionId: String
private let mobileUrl: URL
init(sessionId: String, mobileUrl: URL) {
self.sessionId = sessionId
self.mobileUrl = mobileUrl
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") }
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
// Required so the page can use camera + microphone for liveness
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForMediaPlayback = []
webView = WKWebView(frame: view.bounds, configuration: config)
webView.navigationDelegate = self
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
webView.load(URLRequest(url: mobileUrl))
// Subscribe to SSE in parallel for live status updates
subscribeToSession()
}
private func subscribeToSession() {
let eventsUrl = URL(string: "https://api.tuteliq.ai/api/v1/verify/session/\(sessionId)/events")!
// Use URLSession streaming or a third-party EventSource library
// When status == "completed", call your backend to fetch the verified result
}
}
```
The Info.plist must include `NSCameraUsageDescription` and `NSMicrophoneUsageDescription` for the in-page capture to work.
```kotlin theme={"dark"}
import android.Manifest
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.activity.ComponentActivity
class VerificationActivity : ComponentActivity() {
private lateinit var webView: WebView
private lateinit var sessionId: String
private lateinit var mobileUrl: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sessionId = intent.getStringExtra("sessionId")!!
mobileUrl = intent.getStringExtra("mobileUrl")!!
webView = WebView(this).apply {
settings.javaScriptEnabled = true
settings.mediaPlaybackRequiresUserGesture = false
settings.domStorageEnabled = true
webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
// Grant camera + microphone to Tuteliq's hosted page
request.grant(request.resources)
}
}
loadUrl(mobileUrl)
}
setContentView(webView)
subscribeToSession()
}
private fun subscribeToSession() {
// OkHttp + EventSource extension is the usual choice on Android
// OR poll GET /api/v1/verify/session/{id} every 2-5 seconds
}
}
```
Required permissions in `AndroidManifest.xml`:
```xml theme={"dark"}
```
```typescript theme={"dark"}
import React, { useEffect } from "react";
import { WebView } from "react-native-webview";
import EventSource from "react-native-sse";
type Props = { sessionId: string; mobileUrl: string };
export function VerificationScreen({ sessionId, mobileUrl }: Props) {
useEffect(() => {
const es = new EventSource(
`https://api.tuteliq.ai/api/v1/verify/session/${sessionId}/events`,
);
es.addEventListener("status", (e) => {
const { status } = JSON.parse(e.data);
if (status === "completed" || status === "failed") {
// Ask your backend for the final result
es.close();
}
});
es.addEventListener("result", (e) => console.log("Result:", e.data));
es.addEventListener("expired", () => es.close());
return () => es.close();
}, [sessionId]);
return (
);
}
```
Add to `Info.plist`:
```xml theme={"dark"}
NSCameraUsageDescription
Verify your age with a document and selfie
NSMicrophoneUsageDescription
Required for liveness detection
```
And to `AndroidManifest.xml`:
```xml theme={"dark"}
```
```dart theme={"dark"}
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter_client_sse/flutter_client_sse.dart';
class VerificationScreen extends StatefulWidget {
final String sessionId;
final String mobileUrl;
const VerificationScreen({
super.key,
required this.sessionId,
required this.mobileUrl,
});
@override
State createState() => _VerificationScreenState();
}
class _VerificationScreenState extends State {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.white)
..loadRequest(Uri.parse(widget.mobileUrl));
SSEClient.subscribeToSSE(
method: SSERequestType.GET,
url: 'https://api.tuteliq.ai/api/v1/verify/session/${widget.sessionId}/events',
header: {'Accept': 'text/event-stream'},
).listen((event) {
if (event.event == 'status') {
// Parse event.data, react to status changes
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(body: WebViewWidget(controller: _controller));
}
}
```
Add camera permission to `AndroidManifest.xml` and `Info.plist` as in the React Native example.
```html theme={"dark"}
```
The `allow="camera; microphone"` attribute is required so the embedded page can request capture permissions from the parent origin.
### Pattern B: QR code (desktop-to-mobile handoff)
The session is created on your desktop app or website; the user scans a QR code with their phone and completes verification on the phone.
```typescript Node theme={"dark"}
import QRCode from "qrcode";
const session = await tuteliq.createVerificationSession({ mode: "age" });
// Render the QR code in your UI
const qrSvg = await QRCode.toString(session.url, { type: "svg" });
res.send(`
Scan this code with your phone
${qrSvg}
Session expires in 15 minutes.
`);
// In parallel, subscribe to SSE so the desktop UI updates the moment
// the user completes verification on their phone
```
The user scans, completes capture on their phone, and the desktop UI auto-advances when the SSE stream emits `status: "completed"`.
### Pattern C: Redirect (single-device, browser-only)
Best for low-friction onboarding flows where the user is already on mobile in your web app.
```typescript Node theme={"dark"}
const session = await tuteliq.createVerificationSession({
mode: "age",
redirect_url: "https://yourapp.com/verify/complete?session_id=" + sessionId,
});
res.redirect(session.url);
// After completion, Tuteliq redirects the user to redirect_url
```
The user is sent away to Tuteliq's capture UI; on completion, they're redirected back to your `redirect_url`. Your `redirect_url` handler then calls your backend to read the result.
## Step 3: Watch status updates
Two ways to know when the verification is done. **Server-Sent Events is the recommended primary channel; polling is the fallback.**
### Option A: Server-Sent Events (recommended)
The session emits real-time events you can listen to with no polling overhead. Works from any client (browser, mobile, server).
```typescript Web / browser theme={"dark"}
const es = new EventSource(
`https://api.tuteliq.ai/api/v1/verify/session/${sessionId}/events`,
);
es.addEventListener("status", (e) => {
const { status } = JSON.parse(e.data);
// status: "pending" | "in_progress" | "completed" | "failed" | "expired"
});
es.addEventListener("progress", (e) => {
const { step } = JSON.parse(e.data);
// step: "document_front" | "document_back" | "selfie" | "liveness" | "processing"
});
es.addEventListener("result", (e) => {
const result = JSON.parse(e.data);
// Final verdict + verification record
es.close();
});
es.addEventListener("expired", () => {
// Session expired before user completed it
es.close();
});
```
The events endpoint does **not** require authentication. The session ID has 128 bits of entropy and is single-use; treat it as a secret-equivalent for the lifetime of the session.
### Option B: Polling
If your environment can't hold an open SSE connection (e.g. serverless functions with short timeouts), poll instead:
```typescript Node.js theme={"dark"}
async function waitForSession(sessionId: string, timeoutMs = 600_000) {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const session = await tuteliq.getVerificationSession(sessionId);
if (["completed", "failed", "expired"].includes(session.status)) {
return session;
}
await new Promise(r => setTimeout(r, 3000)); // poll every 3 seconds
}
throw new Error("Session timed out");
}
```
```bash cURL theme={"dark"}
# Poll the session
curl https://api.tuteliq.ai/api/v1/verify/session/$SESSION_ID \
-H "Authorization: Bearer $TUTELIQ_API_KEY"
```
## Step 4: Read the final result
The result is included in the SSE `result` event AND accessible via `GET /api/v1/verify/session/:id` once the session is in a terminal state.
### Age verification result
See the full shape in [Step 3](#3-poll-for-the-result) above. The body is identical whether you receive it through SSE or the polled GET.
### Identity verification result
Identity mode (Assure plan and above) returns the same outer envelope as age mode (`session_id`, `status`, `result`, `created_at`, `expires_at`), but the inner `result` object differs:
```json theme={"dark"}
{
"session_id": "string",
"status": "string (one of: pending, in_progress, completed, failed, expired, cancelled)",
"result": {
"status": "string (one of: verified, failed, needs_review)",
"full_name": "string",
"date_of_birth": "string (ISO 8601 date)",
"document_type": "string (passport, id_card, drivers_license, residence_permit)",
"country_code": "string (ISO 3166-1 alpha-3)",
"liveness": {
"valid": true,
"reason": "string"
},
"face_match": {
"matched": true,
"distance": 0.0,
"confidence": 0.0
},
"credits_used": 0
},
"created_at": 0,
"expires_at": 0
}
```
Compared with age mode, identity mode returns the full `full_name` and `date_of_birth` PII required for KYC, plus a `face_match` score, but does not include the document-validation sub-checks block.
## Status states
| Status | Meaning | What to do |
| ------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `pending` | Session created, user has not opened the URL yet | Wait. The session is valid for 15 minutes. |
| `in_progress` | User has opened the URL and is mid-capture | Show "verification in progress" to the user. Optionally use the `progress` SSE event to display step-by-step UI. |
| `completed` | Verification finished, result available | Read the result and gate access accordingly. |
| `failed` | Verification ran but the user failed (bad liveness, document mismatch, etc.) | Read `failure_reasons` array for the specific issue. Offer the user to retry with a fresh session. |
| `expired` | 15 minutes elapsed without the user finishing | Create a new session and try again. |
| `cancelled` | Your server called `DELETE /api/v1/verify/session/:id`, or the user closed the mobile UI | Optionally create a new session. |
## Common pitfalls
### Webview camera permission
iOS and Android webviews **deny camera access by default**. You must explicitly grant it in both the webview configuration and the host app's permission manifest. The samples above include the right configuration for each platform.
### CORS for the iframe pattern
If you embed via `