{"openapi":"3.1.0","info":{"title":"Tuteliq API","description":"# AI-Powered Child Safety API\n\nYou're building a chat feature for a kids' learning app. A user sends a message that looks innocuous on the surface but contains subtle grooming escalation patterns. Tuteliq catches it, scores the risk by the child's age group, and gives your trust & safety team an actionable report, typically in about a second, or around 400ms in fast mode on a dedicated deployment. One API call replaces months of in-house ML work.\n\n---\n\n## KOSA Harm Categories Coverage\n\n| Harm Category | Endpoint | Status |\n|--------------|----------|--------|\n| Eating Disorders | `/safety/unsafe` | ✅ Covered |\n| Substance Use | `/safety/unsafe` | ✅ Covered |\n| Suicidal Behaviors | `/safety/unsafe` | ✅ Covered |\n| Depression & Anxiety | `/safety/unsafe` + `/analysis/emotions` | ✅ Covered |\n| Compulsive Usage | `/safety/unsafe` | ✅ Covered |\n| Harassment & Bullying | `/safety/bullying` | ✅ Covered |\n| Sexual Exploitation | `/safety/grooming` + `/safety/unsafe` | ✅ Covered |\n| Voice & Audio Threats | `/safety/voice` | ✅ Covered |\n| Visual Content Risks | `/safety/image` | ✅ Covered |\n\n---\n\n## Key Features\n\n- **Age-Calibrated Severity** — Risk scores adjust across four age brackets (under 10, 10–12, 13–15, 16–17) so moderation matches developmental context.\n\n- **Nuanced Context Recognition** — Distinguishes hyperbolic teen language from genuine crisis signals, dramatically reducing false positives.\n\n- **Grooming Pattern Detection** — Identifies multi-stage grooming tactics: trust escalation, secrecy requests, isolation attempts, and boundary violations.\n\n- **Emotional Trend Analysis** — Tracks persistent mood patterns across conversations to surface early indicators of declining mental health.\n\n- **Voice & Image Analysis** — Upload audio or images for transcription, OCR, and full safety analysis in a single call.\n\n- **Real-time Voice Streaming** — WebSocket endpoint for live audio moderation with configurable flush intervals and per-category alerts.\n\n- **Guidance & Reports** — Generate age-appropriate action plans and professional incident reports ready for handoff to human reviewers.\n\n- **Batch Processing & Webhooks** — Analyze up to 50 items per batch call, with HMAC-signed webhook alerts for critical incidents.\n\n---\n\n## Credits per Action\n\n| Endpoint | Credits | Notes |\n|----------|---------|-------|\n| `detectBullying` | 1 | Single text analysis |\n| `detectUnsafe` | 1 | Single text analysis |\n| `detectGrooming` | 1 per 10 msgs | `ceil(messages / 10)`, min 1 |\n| `analyzeEmotions` | 1 per 10 msgs | `ceil(messages / 10)`, min 1 |\n| `getActionPlan` | 2 | Longer generation |\n| `generateReport` | 3 | Structured output |\n| `analyzeVoice` | 5 | Transcription + analysis |\n| `analyzeImage` | 3 | Vision + OCR + analysis |\n\nEvery response includes a `credits_used` field. Credit balance is also available via the `X-Credits-Remaining` response header.\n\n---\n\n## Quick Start\n\n**1.** Get your API key at [tuteliq.ai/dashboard](https://tuteliq.ai/dashboard)\n\n**2.** Install the SDK:\n\n```bash\nnpm install @tuteliq/sdk\n```\n\n**3.** Make your first call:\n\n```typescript\nimport Tuteliq from '@tuteliq/sdk'\n\nconst tuteliq = new Tuteliq({ apiKey: 'YOUR_API_KEY' })\n\nconst result = await tuteliq.detectUnsafe({\n  content: \"Don't talk to your parents about us meeting up\",\n  context: { age_group: \"13-15\" }\n})\n\nconsole.log(result.unsafe)              // true\nconsole.log(result.categories)          // [\"grooming_adjacent\", \"secrecy\"]\nconsole.log(result.severity)            // \"high\"\nconsole.log(result.recommended_action)  // \"Escalate to moderator\"\n```\n\n---\n\n## Performance\n\n| Metric | Value |\n|--------|-------|\n| Full detection, shared tier | ~1.4s typical |\n| Fast mode (`verdict_only`), shared tier | ~1.0s typical |\n| Fast mode, dedicated deployment | ~400ms |\n| Uptime SLA | 99.9% |\n\nShared-tier figures are what a standard API key sees today. The dedicated figure is\nthe same model served on reserved hardware without the shared-tier hop, and is\navailable on enterprise plans rather than by default.\n\nCheck real-time status at [tuteliq.ai/status](https://tuteliq.ai/status)\n","version":"3.4.0","contact":{"name":"Tuteliq Support","url":"https://tuteliq.ai","email":"support@tuteliq.ai"},"license":{"name":"Proprietary","url":"https://tuteliq.ai/terms"}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key as Bearer token"},"apiKeyHeader":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key in header"},"adminSecret":{"type":"apiKey","in":"header","name":"x-admin-secret","description":"Admin secret for privileged operational endpoints"}},"schemas":{}},"paths":{"/live":{"get":{"summary":"Liveness probe","tags":["Health"],"description":"Returns 200 if the service process is running. Use for Kubernetes liveness probe.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]},"timestamp":{"type":"string"}}}}}}}}},"/ready":{"get":{"summary":"Readiness probe","tags":["Health"],"description":"Returns 200 if the service is ready to accept traffic. Checks critical dependencies.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded"]},"timestamp":{"type":"string"}}}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["unhealthy"]},"timestamp":{"type":"string"},"message":{"type":"string"}}}}}}}}},"/health":{"get":{"summary":"Health check","tags":["Health"],"description":"Returns detailed health status of all dependencies","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","unhealthy"]},"timestamp":{"type":"string"},"version":{"type":"string"},"mcp_version":{"type":"string","description":"Version of @tuteliq/mcp this image serves at /mcp"},"detector_version":{"type":"string","description":"Identifier of the deployed detection system. It changes whenever anything that can alter a verdict changes, so a result can be tied to the system that produced it."},"detector_name":{"type":"string","description":"Name of the generation of the deployed detection system. It stays the same across the dated versions within a generation. Tie a result to `detector_version`, not to this."},"build":{"type":"string","description":"The first seven characters of the commit the running image was built from. Present only when the deploy supplied one. The version above names a day; this tells two builds of the same day apart."},"detector_config":{"type":"string","description":"A short fingerprint of the configuration the detection system is running with. It changes whenever that configuration changes, so two deployments of the same version can be told apart."},"uptime":{"type":"number"},"memory":{"type":"object","properties":{"rss_mb":{"type":"number"},"heap_used_mb":{"type":"number"},"heap_total_mb":{"type":"number"},"external_mb":{"type":"number"},"array_buffers_mb":{"type":"number"},"usage_percent":{"type":["null","number"]}}},"checks":{"type":"object","additionalProperties":{"type":"object","properties":{"healthy":{"type":"boolean"},"latencyMs":{"type":"number"},"message":{"type":"string"}}}}}}}}}}}},"/health/dependencies":{"get":{"summary":"Dependency status (admin)","tags":["Health"],"description":"Returns detailed status of each dependency with configuration info. Requires admin authentication via X-Admin-Secret header or Authorization: Bearer token. Other health endpoints (/live, /ready, /health) remain public.","security":[{"adminSecret":[]}],"responses":{"200":{"description":"Default Response"}}}},"/status":{"get":{"summary":"Get API status and component health","tags":["Status"],"description":"Returns overall API status, component-level health (API, database, cache, AI engine), uptime percentages for the last 24h/7d/30d, and any active incidents. Use for status dashboards and monitoring.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/status/ping":{"get":{"summary":"Simple ping for external monitors","tags":["Status"],"description":"Lightweight endpoint returning a minimal OK response. Designed for UptimeRobot, Pingdom, and similar monitoring services that just need a 200 response.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/status/ping/components":{"get":{"summary":"List components available for granular pings","tags":["Status"],"description":"Returns the list of components exposed at `/status/ping/<component>`.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/status/ping/{component}":{"get":{"summary":"Per-component health ping","tags":["Status"],"description":"Returns 200 if the named component is healthy, 503 otherwise. Designed for UptimeRobot, Pingdom and similar. Probe results are cached for ~20 seconds to avoid hammering upstream services when multiple monitors poll.","parameters":[{"schema":{"type":"string"},"in":"path","name":"component","required":true}],"security":[],"responses":{"200":{"description":"Default Response"}}}},"/status/page":{"get":{"summary":"Embeddable HTML status page","tags":["Status"],"description":"Returns a self-contained HTML status page showing component health, uptime percentages, and incident history. Auto-refreshes every 60 seconds. Can be embedded in an iframe on your own status page.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/":{"get":{"summary":"API landing page","tags":["Status"],"description":"Returns an HTML landing page for browsers or a JSON summary for API clients (curl, Postman). Includes links to documentation, dashboard, and current operational status.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/.well-known/oauth-protected-resource":{"get":{"responses":{"200":{"description":"Default Response"}}}},"/.well-known/oauth-protected-resource/mcp":{"get":{"responses":{"200":{"description":"Default Response"}}}},"/.well-known/oauth-authorization-server":{"get":{"responses":{"200":{"description":"Default Response"}}}},"/oauth/register":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/oauth/authorize":{"get":{"responses":{"200":{"description":"Default Response"}}},"post":{"responses":{"200":{"description":"Default Response"}}}},"/oauth/token":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/mcp/":{"get":{"responses":{"200":{"description":"Default Response"}}},"post":{"responses":{"200":{"description":"Default Response"}}},"delete":{"responses":{"200":{"description":"Default Response"}}}},"/mcp/sse":{"get":{"responses":{"200":{"description":"Default Response"}}}},"/mcp/messages":{"post":{"responses":{"200":{"description":"Default Response"}}}},"/api/v1/pricing/":{"get":{"summary":"Get pricing plans","tags":["Pricing"],"description":"Returns all active pricing plans. This endpoint is public and does not require authentication.","security":[],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plans":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"price":{"type":"string"},"period":{"type":"string"},"description":{"type":"string"},"features":{"type":"array","items":{"type":"string"}},"isPopular":{"type":"boolean"},"cta":{"type":"string"},"ctaLink":{"type":"string"}}}}}}}}}}}},"/api/v1/pricing/details":{"get":{"summary":"Get detailed pricing plans","tags":["Pricing"],"description":"Returns detailed pricing plan information including limits and endpoints.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plans":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string"},"description":{"type":"string"},"price_monthly":{"type":"number"},"price_yearly":{"type":"number"},"api_calls_per_month":{"type":"number"},"rate_limit":{"type":"number"},"features":{"type":"array","items":{"type":"string"}},"is_popular":{"type":"boolean"}}}}}}}}}}}},"/api/v1/compliance/dpa":{"get":{"summary":"Get Data Processing Agreement summary","tags":["Compliance"],"description":"Points to the published Tuteliq legal documents and returns the two things a URL cannot convey to a machine: the retention schedule derived from the values the deletion job actually enforces, and each supported data subject right mapped to the live endpoint that exercises it. No authentication required — transparency by design.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/compliance/sub-processors":{"get":{"summary":"List current sub-processors","tags":["Compliance"],"description":"Returns the current list of third-party sub-processors used by Tuteliq, including their purpose and data processing location. Required for GDPR Article 28 transparency. No authentication required.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/compliance/retention":{"get":{"summary":"Get data retention schedule","tags":["Compliance"],"description":"Returns the data retention policy detailing how long each category of data is stored and when it is automatically purged. Covers detection results, audit logs, usage metrics, and account data. No authentication required.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/verify/age/challenge":{"get":{"summary":"Get a signed liveness challenge nonce with randomized sequence","tags":["Age Verification"],"description":"Returns a server-signed nonce and a randomized challenge sequence. The client must follow the prescribed order.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"nonce":{"type":"string"},"signature":{"type":"string"},"expires_at":{"type":"number"},"challenges":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/admin/ids/webhook":{"post":{"summary":"Cloud IDS Pub/Sub push endpoint","tags":["Admin"],"description":"Receives threat notifications from Google Cloud IDS via Pub/Sub push subscription. Verifies the token query parameter before processing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"data":{"type":"string"},"messageId":{"type":"string"},"publishTime":{"type":"string"}}},"subscription":{"type":"string"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"query","name":"token","required":false,"description":"Pub/Sub verification token"}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/wallet/token":{"post":{"summary":"Bootstrap a wallet device via App Attest","tags":["Wallet"],"description":"Issues a short-lived JWT scoped to verify:submit. The iOS Tuteliq Wallet calls this once at first launch and re-uses the token until it nears expiry. Per-device rate limiting prevents bootstrap abuse.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["keyId","attestation","nonce"],"properties":{"keyId":{"type":"string","minLength":8,"maxLength":256},"attestation":{"type":"string","minLength":1,"maxLength":32768},"nonce":{"type":"string","minLength":16,"maxLength":256},"clientId":{"type":"string","minLength":1,"maxLength":256}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expires_at":{"type":"number"},"scope":{"type":"string"},"enforce_app_attest":{"type":"boolean"}}}}}}}}},"/api/v1/prescreen/lexicon":{"get":{"summary":"Fetch the Tuteliq prescreen lexicon","tags":["Prescreen"],"description":"Returns the current SDK-side prescreen lexicon: a set of high-precision phrase patterns mapped to category flags. SDKs run this client-side before calling detection endpoints — clearly-benign content can short-circuit the API call entirely, and positive signals can be attached to the request as structured hints. Public endpoint; lexicon is non-secret reference data.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string","description":"Lexicon schema/content version. Bumped on any change. SDKs cache by this."},"entries":{"type":"array","description":"Pattern entries.","items":{"type":"object","properties":{"id":{"type":"string"},"pattern":{"type":"string"},"type":{"type":"string","enum":["phrase","regex"]},"flag":{"type":"string"},"weight":{"type":"number","description":"-1 for benign, 0..1 for positive signals."},"languages":{"type":"array","items":{"type":"string"}},"endpoints":{"type":"array","items":{"type":"string"}}}}},"usage":{"type":"object","description":"Reference SDK behaviour.","properties":{"short_circuit_when":{"type":"string"},"attach_when":{"type":"string"},"request_field":{"type":"string"}}}}}}}},"304":{"description":"Lexicon unchanged since the If-None-Match etag you supplied."}}}},"/api/v1/account/data":{"delete":{"summary":"Exercise Right to Erasure (GDPR Article 17)","tags":["Account"],"description":"Permanently delete all stored account and analysis data associated with your API key. This is irreversible. All detection results, incidents, emotion records, and usage data will be purged. Returns confirmation and count of deleted records.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"deleted_count":{"type":"number"}}}}}}}},"patch":{"summary":"Rectify account data (GDPR Article 16)","tags":["Account"],"description":"Correct inaccurate personal data stored in your account. Specify the collection, document ID, and fields to update. Changes are audit-logged with the original and new values for compliance.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["collection","document_id","fields"],"properties":{"collection":{"type":"string","description":"Data collection containing the record"},"document_id":{"type":"string","description":"ID of the document to rectify"},"fields":{"type":"object","additionalProperties":true,"description":"Fields to update with corrected values"}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"updated_fields":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/account/export":{"get":{"summary":"Export all account data (GDPR Article 20)","tags":["Account"],"description":"Download a complete JSON export of all data associated with your account: detection results, incidents, emotion analyses, consent records, and usage history. Returned as a downloadable JSON file for data portability.","responses":{"200":{"description":"Default Response"}}}},"/api/v1/account/consent":{"post":{"summary":"Record user consent","tags":["Account"],"description":"Record explicit consent for a specific processing purpose (e.g., data processing, marketing). Stores consent with timestamp, IP address, and user agent for audit compliance. Required before certain data processing activities under GDPR.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["consent_type","version"],"properties":{"consent_type":{"type":"string","description":"Type of consent being recorded"},"version":{"type":"string","description":"Version of the consent policy being accepted"}}}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"consent":{"type":"object","additionalProperties":true}}}}}}}},"get":{"summary":"Get current consent status","tags":["Account"],"description":"Retrieve all active consent records for your account. Optionally filter by consent type. Returns consent type, version accepted, and timestamp for each record.","parameters":[{"schema":{"type":"string"},"in":"query","name":"type","required":false,"description":"Filter by consent type"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"consents":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/v1/account/consent/{type}":{"delete":{"summary":"Withdraw consent for a processing purpose","tags":["Account"],"description":"Withdraw previously given consent for a specific processing type. The withdrawal is recorded with timestamp for audit purposes. Any processing that relied on this consent will cease.","parameters":[{"schema":{"type":"string"},"in":"path","name":"type","required":true,"description":"The consent type to withdraw"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"consent":{"type":"object","additionalProperties":true}}}}}}}}},"/api/v1/account/audit-logs":{"get":{"summary":"View your account audit trail","tags":["Account"],"description":"Retrieve a chronological log of all data actions performed on your account: deletions, exports, consent changes, and rectifications. Supports filtering by action type and result limiting. Essential for demonstrating GDPR compliance.","parameters":[{"schema":{"type":"string"},"in":"query","name":"action","required":false,"description":"Filter by audit action type"},{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"Maximum number of entries to return"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"audit_logs":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/v1/account/api-keys/quota":{"get":{"summary":"Get API key count quota for current tier","tags":["Account"],"description":"Return the number of currently active API keys for the authenticated account, the per-tier maximum, and whether another key can be created. Dashboards must call this before minting a new key to enforce the plan limit.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"tier":{"type":"string"},"active_keys":{"type":"number"},"max_keys":{"type":["number","string"],"description":"Integer or \"unlimited\" for enterprise/test tiers"},"can_create":{"type":"boolean"},"links":{"type":"object","properties":{"upgrade":{"type":"string"}}}}}}}}}}},"/api/v1/account/mcp-tokens":{"get":{"summary":"List MCP session tokens","tags":["Account"],"description":"List all MCP tokens associated with your account. These are scoped tokens issued to MCP clients like Claude Desktop.","responses":{"200":{"description":"Default Response"}}}},"/api/v1/account/mcp-tokens/{tokenId}":{"delete":{"summary":"Revoke an MCP session token","tags":["Account"],"description":"Revoke a specific MCP token. The MCP client using this token will be disconnected.","parameters":[{"schema":{"type":"string"},"in":"path","name":"tokenId","required":true}],"responses":{"200":{"description":"Default Response"}}},"patch":{"summary":"Rename an MCP session token","tags":["Account"],"description":"Update the display name of an MCP token.","parameters":[{"schema":{"type":"string"},"in":"path","name":"tokenId","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/account/encryption-key":{"get":{"summary":"Get the customer-registered public key for end-to-end incident encryption","tags":["Account"],"description":"Returns the public key currently used to encrypt incident records, or 404 if none is registered. When no key is registered, incidents fall back to server-side encryption (Tuteliq holds the key). Register a public key with POST to switch to true end-to-end encryption — only your dashboard can decrypt.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"algorithm":{"type":"string"},"public_key_pem":{"type":"string"},"key_fingerprint":{"type":"string","description":"sha256(DER(SPKI)).hex — verify on your side to confirm integrity"},"registered_at":{"type":"string"},"rotated_from":{"type":"string"}}}}}}}},"post":{"summary":"Register (or rotate) the public key Tuteliq uses to encrypt your incident records","tags":["Account"],"description":"Registers an RSA public key. After registration, all NEW incident records (rationale, visual_description, etc.) are encrypted with a per-record AES key, which is itself wrapped with this RSA key (hybrid scheme RSA-OAEP + AES-256-GCM). Tuteliq cannot decrypt — only your dashboard (with the matching private key) can.\n\nRotating: POST a new key here. New incidents use the new key; old incidents remain readable with the old private key until they expire (90 days).\n\nPrivate key is YOUR responsibility — Tuteliq cannot recover lost-key incidents.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["algorithm","public_key_pem"],"properties":{"algorithm":{"type":"string","enum":["RSA-OAEP-2048","RSA-OAEP-4096"],"description":"RSA-OAEP key size. 2048-bit is faster, 4096-bit is more conservative."},"public_key_pem":{"type":"string","description":"PEM-encoded SPKI public key (begins with \"-----BEGIN PUBLIC KEY-----\").","maxLength":8192}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"algorithm":{"type":"string"},"public_key_pem":{"type":"string"},"key_fingerprint":{"type":"string"},"registered_at":{"type":"string"},"rotated_from":{"type":"string"}}}}}}}},"delete":{"summary":"Revoke the customer-registered public key","tags":["Account"],"description":"After revocation, new incidents fall back to server-side encryption (Tuteliq holds the key). Existing incidents stay encrypted under the previous key and remain readable only with its private key. Common reason to revoke: key compromise (then immediately register a new one).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/api/v1/safety-classification":{"get":{"summary":"Get this account's Tuteliq Platform Safety Classification","tags":["Safety"],"description":"Returns the saved daily snapshot of the account's operational safety classification, the same figure shown on the dashboard and the public Trust Center. It is computed once daily and read here as stored; this endpoint never recomputes or adjusts it, so all three surfaces always agree. `date` shows which day the snapshot is from. Returns 200 with `score: null` and `insufficient_data: true` when the account has too little activity to classify, and 404 when it has no classification at all. Neither is a zero score.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string","description":"ISO date (UTC) the snapshot was computed. Always present, so a caller can see the figure is a daily snapshot rather than live."},"score":{"type":["number","null"],"description":"0-100. `null` when `insufficient_data` is true. Never treat null as zero."},"band":{"type":["string","null"],"description":"`leading` >= 85, `established` 70-84, `progressing` 45-69, `needs_attention` < 45. `null` when insufficient."},"insufficient_data":{"type":"boolean","description":"True when fewer than 5 real incidents have been processed. The account is not yet classified; it has not scored badly."},"reason":{"type":["string","null"],"description":"Why the account is not yet classified."},"period":{"type":"string","description":"Always `all_time`."},"components":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"reviewCoverage | responseTime | openQueueAge | detectionCoverage | integrationHealth"},"label":{"type":"string"},"points":{"type":"number"},"max":{"type":"number"},"tip":{"type":["string","null"],"description":"Improvement hint, present when the component is below full marks."}}}},"evidence":{"type":"object","description":"The inputs the score was computed from. Contains no personal data: no names, emails or message content.","properties":{"incident_count":{"type":"number","description":"Real incidents counted. Test, sandbox and demo traffic is excluded by the job."},"reviewed_count":{"type":"number","description":"Incidents whose status is not `new`."},"median_review_minutes":{"type":["number","null"]},"oldest_open_days":{"type":["number","null"],"description":"Age of the oldest `new` or `escalated` incident. 0 when the queue is empty."},"modalities_live":{"type":"array","items":{"type":"string"}},"applicable_modalities":{"type":"array","items":{"type":"string"},"description":"Content types the platform offers. Scoring is measured only against these."},"connected_platforms":{"type":"number"},"calculated_at":{"type":"string"}}},"calculated_at":{"type":"string"},"updated_at":{"type":"string"},"disclaimer":{"type":"string","description":"Carried in the payload deliberately. The dashboard card shows this next to the number; an API caller has no card, and a score published without it can be mistaken for a certification."}}}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Always `no_classification`."},"message":{"type":"string"}}}}}}}}},"/api/v1/admin/breach/":{"post":{"summary":"Log a new data breach incident","tags":["Admin"],"description":"Record a data breach for GDPR Article 33/34 compliance. Captures breach details, severity, affected users, and data categories involved. Automatically timestamps the report and initializes notification tracking. Use this as the first step in your breach response workflow.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","description","severity","affected_user_ids","data_categories","reported_by"],"properties":{"title":{"type":"string","description":"Brief title describing the breach"},"description":{"type":"string","description":"Detailed description of what happened"},"severity":{"type":"string","description":"Breach severity level"},"affected_user_ids":{"type":"array","items":{"type":"string"},"description":"List of affected user IDs"},"data_categories":{"type":"array","items":{"type":"string"},"description":"Categories of data affected (e.g., personal_data, analysis_results)"},"reported_by":{"type":"string","description":"Identifier of the person reporting the breach"}}}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"breach":{"type":"object","additionalProperties":true}}}}}}}},"get":{"summary":"List all recorded breach incidents","tags":["Admin"],"description":"Retrieve breach records with optional filtering by status (open, investigating, resolved, closed). Returns breach details, severity, affected user count, and current notification status. Use for breach dashboard and compliance reporting.","parameters":[{"schema":{"type":"string"},"in":"query","name":"status","required":false,"description":"Filter by breach status"},{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"Maximum number of records to return"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"breaches":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/v1/admin/breach/{id}":{"get":{"summary":"Get breach incident details","tags":["Admin"],"description":"Retrieve full details of a specific breach incident including timeline, affected users, data categories, notification status, and any notes added during investigation.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Breach incident ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"breach":{"type":"object","additionalProperties":true}}}}}}}},"patch":{"summary":"Update breach status and notification progress","tags":["Admin"],"description":"Update a breach incident's status (investigating, resolved, closed) and notification progress. Add notes to document investigation findings or remediation steps. All updates are timestamped for the breach audit trail.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"New breach status"},"notification_status":{"type":"string","description":"Notification progress (e.g., pending, sent, acknowledged)"},"notes":{"type":"string","description":"Investigation notes or remediation details"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Breach incident ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"breach":{"type":"object","additionalProperties":true}}}}}}}}},"/api/v1/admin/retention/cleanup":{"post":{"summary":"Trigger manual data retention cleanup","tags":["Admin"],"description":"Manually run the data retention cleanup process to purge records that have exceeded their retention period. Normally runs on a schedule, but can be triggered on-demand for compliance audits or storage management. Returns a summary of deleted records by category.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"result":{"type":"object","additionalProperties":true}}}}}}}}},"/api/v1/admin/retention/phash":{"delete":{"summary":"Delete known-synthetic perceptual hashes","tags":["Admin"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hashes"],"properties":{"hashes":{"type":"array","items":{"type":"string"},"maxItems":100}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"number"},"message":{"type":"string"}}}}}}}}},"/api/v1/admin/ids/threats":{"get":{"summary":"List Cloud IDS threats","tags":["Admin"],"description":"Retrieve stored IDS threat events with optional filtering by severity, date range, and limit.","parameters":[{"schema":{"type":"string"},"in":"query","name":"severity","required":false,"description":"Filter by severity (e.g., CRITICAL, HIGH, MEDIUM, LOW)"},{"schema":{"type":"string"},"in":"query","name":"since","required":false,"description":"ISO date — only return threats since this date"},{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"Maximum number of records to return"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"threats":{"type":"array","items":{"type":"object","additionalProperties":true}},"total":{"type":"number"}}}}}}}}},"/api/v1/admin/ids/threats/{id}":{"get":{"summary":"Get Cloud IDS threat details","tags":["Admin"],"description":"Retrieve full details of a specific IDS threat event including raw log data.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"Threat record ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"threat":{"type":"object","additionalProperties":true}}}}}}}}},"/api/v1/admin/ids/stats":{"get":{"summary":"Cloud IDS threat statistics","tags":["Admin"],"description":"Returns summary counts of IDS threats by severity and time range.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"by_severity":{"type":"object","properties":{"critical":{"type":"number"},"high":{"type":"number"},"medium":{"type":"number"},"low":{"type":"number"},"informational":{"type":"number"}}},"last_24h":{"type":"number"},"last_7d":{"type":"number"}}}}}}}}},"/api/v1/verify/session/{id}/validate-document":{"post":{"summary":"Validate and process a document image via session (mobile)","tags":["Verification"],"description":"Per-step document validation for the cross-device flow. Checks image quality, then runs the document work (text extraction, face template) so that the submit step does not need the document image again: the session carries the result, never the image, until the session is submitted, cancelled, or expires. Send `side` (`front`, `back` or `passport`) and `document_type` as form fields ahead of the file. `retained: false` means the document was not held (encryption unavailable, or the document could not be processed) and must be included in the submit. Free, no credits consumed.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"quality":{"type":"string","enum":["good","poor","invalid"]},"issues":{"type":"array","items":{"type":"string"}},"retained":{"type":"boolean"}}}}}}}}},"/api/v1/verify/session/{id}/validate-selfie":{"post":{"summary":"Validate and assess a selfie via session (mobile)","tags":["Verification"],"description":"Per-step selfie validation for the cross-device flow. Always checks that a face is present. When the request also carries the liveness capture (`liveness_frame` files and the `liveness_token` field), it runs the selfie assessment now (liveness, age estimation, and the face match against the document already processed by validate-document) and the session carries the DECISION, never the selfie or the frames, to submit, which then needs no images. `assessed: false` means the assessment did not run (no capture sent, or nothing to match against) and the submit must carry the selfie and liveness as before. Free, no credits consumed.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"face_detected":{"type":"boolean"},"message":{"type":["string","null"]},"assessed":{"type":"boolean"}}}}}}}}},"/api/v1/verify/session/{id}/submit":{"post":{"summary":"Submit verification via session (mobile)","tags":["Verification"],"description":"Complete a verification using a session token. When validate-document and validate-selfie already processed the document and the selfie for this session, this request needs no images at all and returns immediately. Otherwise send the selfie and liveness capture (and, unless validate-document processed them, the ID document images). Images included in this request always take precedence over what the session carries.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"status":{"type":"string"}}}}}}}}},"/api/v1/verify/session/{id}/progress":{"post":{"summary":"Report verification step progress from mobile","tags":["Verification"],"description":"Used by mobile device to report which step the user is on, so desktop can show real-time progress.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["step"],"properties":{"step":{"type":"string"}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/api/v1/verify/session/{id}/document-status":{"get":{"summary":"Which document sides the session already holds a processed result for","tags":["Verification"],"description":"Session-token authenticated. Used by a device that resumed past the document steps while the other device was still reading the document: it polls this until the sides it needs are held, then continues without capturing the document again. Never returns document content.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"document_sides_held":{"type":"array","items":{"type":"string","enum":["front","back","passport"]}}}}}}}}}},"/api/v1/verify/session/{id}/mode":{"get":{"summary":"Resolve verification mode and language for end-user","tags":["Verification"],"description":"Called by the HTML verification page on load. Detects the end-user country from IP, resolves the verification mode from market config, and returns the UI language. No authentication required — session ID is unguessable (128-bit entropy).","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_mode":{"type":"string","enum":["full","liveness_only"]},"country":{"type":["string","null"]},"region":{"type":["string","null"]},"market":{"type":["string","null"]},"lang":{"type":"string"},"age_threshold":{"type":"number"},"recommended_image_width":{"type":"integer"}}}}}}}}},"/api/v1/verify/session/{id}/events":{"get":{"summary":"Subscribe to session status updates via SSE","tags":["Verification"],"description":"Server-Sent Events stream that emits status changes and results for a verification session. No authentication required — session ID is unguessable (128-bit entropy).","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/safety/bullying":{"post":{"summary":"Detect bullying, harassment, and intimidation in text","tags":["Safety"],"description":"Analyze a message for bullying indicators including verbal abuse, social exclusion, threats, and cyberbullying patterns. Returns severity (low/medium/high/critical), bullying type classification, confidence score, and a recommended action. Risk scores are age-calibrated when context.ageGroup is provided.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The message text to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"ageGroup":{"type":"string","description":"Age group (e.g., \"11-13\")"},"age_group":{"type":"string","description":"Alias for `ageGroup`, as the other detectors spell it. When both are given, `ageGroup` is used."},"relationship":{"type":"string","description":"Relationship between participants"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."}}},"options":{"type":"object","properties":{"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Minimum severity to show crisis support resources (default: high). Critical always shows."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: skip generating `action_detail`, the moderator-guidance expansion of `recommended_action`. Lower latency and a smaller response; `rationale` (used for incident summaries) and the verdict itself (severity, categories, recommended_action, risk_score) are unchanged."},"flag_profanity":{"type":"boolean","description":"Additive, deterministic word-list flag for plain profanity/vulgarity. Adds `profanity` to the response; never affects `is_bullying`, `severity`, `risk_score`, or `recommended_action`. Free — no extra credits. Not a harm classifier: does not cover slurs or hate speech, which the detector itself already handles with full context."},"flag_risk_terms":{"type":"boolean","description":"Additive, deterministic word-list flag for bare drug and violence terms (e.g. \"cocaine\", \"kill\"). Adds `risk_terms` to the response; never affects `is_bullying`, `severity`, `risk_score`, or `recommended_action`. Free — no extra credits. Purely lexical: WILL fire on benign uses of an included term (\"kill the lights\") since it cannot read context the way the detector itself does — combine with the detector’s own verdict for a stricter policy, don’t use in isolation as a block decision."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"continuation_token":{"type":"string","description":"Opaque signed token from a previous /bullying call. Carries derived analysis state — no message content is stored server-side."},"reset_conversation":{"type":"boolean","description":"Discard any continuation_token and start a fresh conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side message id for evidence pointers."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence over the account-level flag for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Lets you control incident logging per request, e.g. suppress logging for test traffic."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"speaker_role":{"type":"string","enum":["enacting","experiencing","reporting_other","self_directed","unclear"],"description":"The speaker's relationship to the conduct described, so a report can be routed to a victim-protection queue rather than a sender-sanction one without parsing rationale prose. `enacting` = the speaker is bullying. `experiencing` = the speaker describes what was done to them. `reporting_other` = a bystander describing a third party's conduct, or relaying someone else's decision. `self_directed` = the speaker disparages only themselves. `unclear` = cannot be determined. Descriptive: this never changes `is_bullying`, `severity`, `risk_score` or `recommended_action`. It is the ONLY field that distinguishes the person bullying from the person being bullied, since `is_bullying` reports presence for both. Same vocabulary as `speaker_role` on /safety/coercive-control."},"is_bullying":{"type":"boolean","description":"Whether a bullying signal is PRESENT in the content, at ANY severity (including low / monitor cases). Presence, not authorship: this is `true` for a child describing bullying being done TO them, exactly as it is for the person doing it. Use `speaker_role` to tell those apart before you act on it, or you will sanction the victim. For production branching prefer `recommended_action` (`flag_for_review` / `block` / `immediate_intervention`), which is a stable enum, over the bare presence of a signal."},"bullying_type":{"type":"array","items":{"type":"string","enum":["exclusion","verbal_abuse","humiliation","threat","intimidation","sexual_harassment","body_shaming","identity_based","impersonation","outing","pile_on","cyberstalking","exclusion_threat","coded_harassment","cyberbullying","relational"]},"description":"Canonical vocabulary. Values outside this enum are dropped before the response is built, so this field is safe to switch on."},"confidence":{"type":"number"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"rationale":{"type":"string"},"recommended_action":{"type":"string","enum":["none","monitor","flag_for_review","block","immediate_intervention"],"description":"Routing key, weakest to strongest: `none`, `monitor`, `flag_for_review`, `block`, `immediate_intervention`. Safe to switch on."},"action_detail":{"type":"string","description":"Optional human-readable expansion of `recommended_action` for a moderator UI. Free text: do not branch on it."},"risk_score":{"type":"number"},"language":{"type":"string","description":"Language code used for analysis (auto-detected or provided)"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity: \"stable\" (en) or \"beta\" (all others)"},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"continuation_token":{"type":"string","description":"Signed token carrying derived analysis state for the next call. Server stores nothing."},"continuation_expires_at":{"type":"string","description":"ISO 8601 expiry timestamp of the continuation_token."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How prior state was sourced."},"trajectory_risk":{"type":"number","description":"Conversation-level risk 0-1, distinct from `risk_score` which scores only this message. Anchored on the highest severity seen so far and decaying slowly across benign turns, so a friendly message after an escalation does not reset it. Absent on the first turn. Derived from the signed token: no message content is stored to produce it."},"trajectory":{"type":"string","enum":["rising","stable","declining","none"],"description":"Direction of travel across the conversation so far."},"severity_series":{"type":"array","items":{"type":"number"},"description":"Per-turn severity, oldest first — the evidence behind `trajectory_risk`, so a moderator can see why a benign-looking message carries elevated conversation risk."},"escalation_capped":{"type":"boolean","description":"True when a coded-term match pushed severity toward critical but this endpoint's corroboration-cap logic held recommended_action below immediate_intervention pending independent confirmation. Absent when no cap applied."},"escalation_capped_reason":{"type":"string","description":"Human-readable explanation of the cap. Present only when escalation_capped is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute. Absent otherwise."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"passing_remark_capped":{"type":"boolean","description":"True when a review alert was lowered to `monitor` because a separate reading found a remark about how someone is playing or performing, or a reaction to what they are doing in the moment, and nothing about the person themselves: no slur, no threat, nobody else urged to act, no sign it is repeated. Also set, with its own reason, where a separate reading found something being kept from a person in order to please them (a surprise, a present), or a rough word said together with plain affection, and nothing in the message going at the person. Trash talk about play is not bullying on its own. It applies only to a `flag_for_review` verdict that names no threat, identity attack, pile-on or other severe type, with no earlier turns in play; anything stronger is never lowered. It never clears to `none`, so a pattern across a conversation stays visible. Absent unless it applied."},"passing_remark_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when passing_remark_capped is true."},"single_insult_watched":{"type":"boolean","description":"True when the analysis found bullying by the speaker and asked for no action (one isolated, low-severity insult), and `recommended_action` was set to `monitor` so that it can be counted. It is not sent for review. Absent otherwise."},"single_insult_watched_reason":{"type":"string","description":"Human-readable explanation. Present only when single_insult_watched is true."},"bullying_report_surfaced":{"type":"boolean","description":"True when the analysis asked for nothing above `monitor` and a separate reading found the speaker telling of a specific person, themselves or someone they know, being repeatedly or seriously mistreated by peers, with nothing saying it has stopped. The result is sent for review as a report to route to support, not as an action against the person writing. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"bullying_report_surfaced_reason":{"type":"string","description":"Human-readable explanation. Present only when bullying_report_surfaced is true."},"rumour_spread_raised":{"type":"boolean","description":"True when the analysis asked for nothing above `monitor` and a separate reading found the writer passing on, or hinting at, something about a specific person who is not there that would lower them in the eyes of others, to people other than that person. The result is sent for review as a rumour being spread. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"rumour_spread_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when rumour_spread_raised is true."},"looks_attack_raised":{"type":"boolean","description":"True when the analysis asked for nothing above `monitor` and a separate reading found a put-down about the body, weight, face or looks of the person written to or of a specific named person, whatever disclaimer or affection followed it. The result is sent for review. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"looks_attack_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when looks_attack_raised is true."},"continued_after_objection_raised":{"type":"boolean","description":"True when the analysis asked for nothing above `monitor`, the text reads as a conversation, and a separate reading found that one person objected (asked the other to stop, said it hurt, asked not to be shared or repeated, or asked to be included) and the other carried on. Sent for review, never more. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"continued_after_objection_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when continued_after_objection_raised is true."},"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text working against the harm it describes: teaching, moderating, counselling, reporting or asking for advice. Two outcomes. When nobody is at risk in the text and the speaker is describing someone else, nothing is detected and the action is `monitor`. When a person is at risk and a responsible adult in the text is already acting, an urgent action becomes `flag_for_review` with the severity and score brought down, and never lower. It never applies to a person speaking about their own risk, to a verdict supported by an acute marker, or when earlier turns are in play. Absent unless it applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"number_coincidence_cleared":{"type":"boolean","description":"True when an alert rested on a number that is also known as a code, and a separate reading found the number in ordinary use (an address, a count, a price, a room) with nothing else harmful in the content. The verdict was cleared. Absent when this did not happen."},"number_coincidence_cleared_reason":{"type":"string","description":"Human-readable explanation. Present only when number_coincidence_cleared is true."},"profanity":{"type":["object","null"],"description":"Present only when options.flag_profanity (or the account-level default_flag_profanity setting) is true. Deterministic word-list result — additive, never affects is_bullying/severity/risk_score/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"string"}}}},"risk_terms":{"type":["object","null"],"description":"Present only when options.flag_risk_terms (or the account-level default_flag_risk_terms setting) is true. Deterministic drug/violence word-list result — additive, never affects is_bullying/severity/risk_score/recommended_action. Purely lexical: can fire on benign uses of an included term.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"category":{"type":"string","enum":["drug","violence"]}}}}}},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only included for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}}}}}}}}}},"/api/v1/safety/grooming":{"post":{"summary":"Detect grooming patterns in conversations","tags":["Safety"],"description":"Analyze a sequence of messages for multi-stage grooming tactics: trust building, isolation, secrecy requests, boundary testing, and sexual escalation. Accepts full conversation history with sender roles to detect patterns that single-message analysis would miss. Returns grooming risk level, identified tactics as flags, and recommended intervention actions.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["sender_role","text"],"properties":{"sender_role":{"type":"string","description":"Role of sender (e.g., \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"sender_age":{"type":"number","description":"Optional numeric age of THIS message's sender. Strengthens age-gap analysis without forcing the model to infer it from the role label."}}},"description":"Sequence of messages to analyze. Consecutive same-role messages of ≤3 characters are automatically defragmented before tactic analysis to prevent letter-by-letter evasion."},"continuation_token":{"type":"string","description":"Opaque signed token from a previous /grooming call. Carries derived analysis state across calls without server-side storage."},"reset_conversation":{"type":"boolean","description":"Discard any continuation_token and start a fresh conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this batch (used as the evidence pointer in the issued continuation_token)."},"context":{"type":"object","properties":{"child_age":{"type":"number","description":"Age of the child / minor in the conversation."},"participant_age":{"type":"number","description":"Optional age of the non-minor participant. When known (e.g. on age-verified platforms), this lets the model compute the actual age gap rather than infer it."},"platform":{"type":"string","description":"Platform name"},"language":{"type":"string","description":"Language code (e.g., \"en\")"},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."}}},"options":{"type":"object","properties":{"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Minimum severity to show crisis support resources (default: high). Critical always shows."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array and `action_detail` from the response, reducing output length and latency. `rationale` (used for incident summaries) is always generated."},"include_relationship_frame":{"type":"boolean","default":false,"description":"Opt in to the descriptive `relationship_frame` response field. Off by default because it costs one extra narrow model call. Ignored in `verdict_only` mode, and only computed when at least one tactic was flagged. It never changes the verdict; see the field description."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence over the account-level flag for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Lets you control incident logging per request, e.g. suppress logging for test traffic."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"grooming_risk":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Grooming-risk level. \"low\" indicates a signal worth monitoring but no action required; branch your alerting on `recommended_action` (`flag_for_review` / `block` / `immediate_intervention`), not the bare presence of a low-level risk."},"verdict_withdrawn":{"type":"boolean","description":"True when the analysis returned a high or critical verdict with no tactic and no risk score to support it. The verdict was reconciled to what the response actually evidences and recommended_action brought down to review, rather than returning two fields that contradict each other. Never fires on a finding that names a tactic or carries any score. Absent unless it applied."},"verdict_withdrawn_reason":{"type":"string","description":"Human-readable explanation. Present only when verdict_withdrawn is true."},"number_coincidence_cleared":{"type":"boolean","description":"True when an alert rested on a number that is also known as a code, and a separate reading found the number in ordinary use (an address, a count, a price, a room) with nothing else harmful in the content. The verdict was cleared. Absent when this did not happen."},"number_coincidence_cleared_reason":{"type":"string","description":"Human-readable explanation. Present only when number_coincidence_cleared is true."},"verdict_capped":{"type":"boolean","description":"True when the analysis returned a high or critical verdict carrying a risk score but naming no tactic to support it. The verdict is capped to medium and recommended_action to flag_for_review, so the conversation still reaches a reviewer rather than routing an escalation a reviewer cannot check; risk_score is capped to the top of the medium band. A medium verdict naming no tactic keeps its level and action and has only its risk_score capped to the top of the medium band. Where, in addition, no child was declared or stated anywhere, none of the withdrawn claims was a tactic that presupposes a child, no refusal or threat was reported and a separate reading found no sign of a minor and no pressure, the verdict is lowered to low and recommended_action to monitor: kept visible, not sent for review. `verdict_capped_reason` says which applied. Never fires on a finding that names a tactic. Only ever lowers, never raises. Absent unless it applied."},"verdict_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when verdict_capped is true."},"affection_only_capped":{"type":"boolean","description":"True when an alert that named no tactic was lowered to `low` / `monitor` because a separate reading of the conversation found nothing in it but warmth and everyday logistics: greetings, terms of endearment, kisses, \"love you\", checking where someone is. A parent calling a child \"baby\" is the case this exists for. It applies ONLY when no tactic survived verification; one verified tactic and it does nothing. It never clears to `none`, so a pattern can still build across a conversation. Absent when it did not apply."},"affection_only_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when affection_only_capped is true."},"adult_exchange_capped":{"type":"boolean","description":"True when an alert was lowered to `low` / `monitor` because the people in the conversation are adults. It applies only when the request declared no child (no `child_age` under 18, no child `sender_role`, no `sender_age` under 18), nobody states an age under 18, and no tactic that presupposes a child was verified; a separate reading must then find that the people taking part state ages of 18 or more (or every message carries a `sender_age` of 18 or more), nothing indicating that a participant is or may be under 18, and no pressure, threat or refusal. A conversation in which nobody states an age keeps its alert. Grooming is a harm done to a child, and a willing conversation between adults is not one. If your platform knows a participant is a minor, declare it and this never applies. It never clears to `none`. Absent unless it applied."},"adult_exchange_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when adult_exchange_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when a conversation about a young person who has been approached was kept at `monitor` and not raised to review, because your platform declared a `context.conversation_type` that names the responsible party in the conversation and that party is already handling it. Never `none`. In `education` and `family` this applies only when every message is an adult's. Absent otherwise."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when protective_speech_capped is true."},"shared_surprise_capped":{"type":"boolean","description":"True when a review alert was lowered to `low` / `monitor` because the only thing kept quiet in the conversation is a surprise for the person it is kept from (a present, a party), a parent or carer of the child is part of the plan, and the adult asks for nothing alone, personal or deleted. It applies ONLY to `flag_for_review` below critical, and only when a secrecy request and boundary pushing are the only tactics that survived verification: a photo request, a meeting, a gift to the child, isolation or flattery blocks it. A separate reading must find all three things; anything else, or no answer, keeps the alert. The tactics stay in `flags`. What defines this harm is an adult keeping their contact with a child from the people who protect the child. Absent when it did not apply."},"shared_surprise_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when shared_surprise_capped is true."},"meeting_logistics_cleared":{"type":"boolean","description":"True when a single submitted message was flagged for `meeting_request` and nothing else, and a separate reading found that it only gives, asks for or arranges practical details for an ordinary purpose. The result is cleared: `grooming_risk` none, no flags, no action. Never applied to a conversation of more than one message, to a result that names anything else, or to the most urgent action. Absent otherwise."},"meeting_logistics_cleared_reason":{"type":"string","description":"Human-readable explanation. Present only when meeting_logistics_cleared is true."},"long_conversation_raised":{"type":"boolean","description":"True when a long conversation was read a second time with a few of its messages singled out for close reading, and the result shown is that second reading of the whole conversation. Only ever raises a result, never lowers one. Absent otherwise."},"long_conversation_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when long_conversation_raised is true."},"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult child_age but a participant states a minor age in the conversation itself. The declared age was not used. Nothing is escalated by this on its own; the analysis simply runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says in the chat is what a bypassed age verification looks like. A stated ADULT age never has this effect, because message text is attacker-controlled."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"confidence":{"type":"number"},"flags":{"type":"array","items":{"type":"string","enum":["secrecy_request","boundary_pushing","photo_request","gift_giving","isolation","flattery","sexual_content","meeting_request","reconnaissance","sexualization","grooming","sextortion"]},"description":"Closed vocabulary. Values outside this list are dropped before the response is built, so this field is safe to switch on. Tuteliq's published grooming taxonomy is EIGHT per-message tactics: `flattery`, `photo_request`, `gift_giving`, `secrecy_request`, `meeting_request`, `isolation`, `boundary_pushing` and `reconnaissance`. Those are the eight to quote, and the ones the model card reports. `sexual_content` also appears here and is NOT a tactic: it is a content category sharing the array. `reconnaissance` is probing whether a child is unsupervised, alone or reachable: who oversees the child's device or person (supervision), or when the child is alone, free or unobserved (availability). As of 25 September 2026 it is emitted PER MESSAGE on the turn carrying the probe, including probes phrased as statements. A question that is useless to the asker unless the answer is \"yes, supervised\" is a safeguarding check and is deliberately NOT flagged, so a tutor or coach confirming a guardian will be present does not produce it. CONVERSATION-LEVEL ACCUMULATION IS A SEPARATE CAPABILITY AND IS NOT YET SCORED THAT WAY. This field tells you a given message carried a probe; it does not tell you how many probes make a pattern, and no arc-level threshold is applied. Do not read a single `reconnaissance` as a completed profiling arc, and do not read the absence of `reconnaissance` across a conversation as the absence of probing. `sexualization` and `sextortion` reach `flags` by a different route, coded-term matching. Those two assert something weaker than a tactic: that the conversation contains a recognised coded term of that kind, confirmed in context. `grooming` remains in the vocabulary and is not currently emitted. If you are reporting on the taxonomy, use the eight."},"relationship_frame":{"type":"string","enum":["established_offline","online_only","unestablished"],"description":"What the CONVERSATION CONTENT establishes about how the two participants know each other, reported factually so a finding can be routed. `established_offline` = they already knew each other in the physical world before this conversation (school, club, lesson, household, a stated and evidenced role). `online_only` = the contact exists only online or began online, INCLUDING a conversation working toward a first meeting. `unestablished` = the content settles neither. DESCRIPTIVE ONLY. This never changes `grooming_risk`, `risk_score`, `recommended_action` or `flags`, in the same way `speaker_role` is descriptive on /safety/bullying. DO NOT TREAT `established_offline` AS REASSURING, and do not build a rule that lowers risk because an adult is known to the child: most child sexual abuse is committed by someone the child already knows. Measured on the 270-conversation PAN12 problem-2 set, `online_only` was two and a half times MORE common in non-predatory conversations than predatory ones (0.517 vs 0.193), and `established_offline` was three times more common in predatory ones (0.173 vs 0.058). The stranger intuition runs backwards in real transcript data. Present only when at least one tactic was flagged, since it exists to route a finding. Absent if the classification fails."},"disclosure_cleared":{"type":"boolean","description":"True when `grooming_risk` was cleared to `none` because this conversation is a DISCLOSURE, not an incident: every tactic the analysis claimed was withdrawn as described rather than performed, and `speaker_stance` is `describing_third_party`. A child telling someone for the first time, a bystander report, or a parent reporting what their child told them. The grooming verdict is the only thing cleared. `recommended_action` is deliberately NOT lowered, because a disclosure still needs to reach a person: treat it as a support and safeguarding route rather than a moderation one, and never action the speaker. Absent when the clear did not apply."},"disclosure_cleared_reason":{"type":"string","description":"Human-readable explanation of why `disclosure_cleared` was applied. Present only alongside it."},"confidentiality_frame":{"type":"string","enum":["unconditional_concealment","limited_confidentiality","guardian_included","none"],"description":"The adult's own framing of who may know, described factually rather than judged. `unconditional_concealment` asks the child to keep something from someone with no stated limit; `limited_confidentiality` offers privacy but names a condition under which the adult would tell someone (\"unless someone's at risk\"), which is safeguarding practice rather than a secrecy demand; `guardian_included` tells the child to inform a parent or carer, or says the adult will."},"secrecy_frame_cleared":{"type":"boolean","description":"True when `secrecy_request` was removed from `flags` because the adult stated the limits of confidentiality or included a guardian. Routing is unaffected: the secrecy_request + isolation escalation reads the pre-removal flag list, so this can never lower an action. Absent when nothing was removed."},"flag_attribution":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Which participant each flag is attributed to: tactic -> the `sender_role` values of the messages that actually carry it. Derived from the per-message analysis, so it answers \"who is doing this?\" rather than assuming the non-child role. OMITTED ENTIRELY when `speaker_stance` is `describing_third_party`, because a speaker reporting someone else's conduct is not performing it. A child disclosing abuse or reporting a contact would otherwise be named as the author of the tactics they are describing, and a platform actioning this field would sanction the person who asked for help. Use `speaker_stance` for those conversations. Also omitted when the model produced no per-message tags. Attribution only; it does not change the verdict or imply the other party is blameless."},"ungrounded_flags":{"type":"array","items":{"type":"string"},"description":"Conversation-level tactics the model asserted but that could not be supported: either tied to no specific message at all, or tied only to messages that a separate adjudication check found do not express that tactic (#262). Removed from `flags` and surfaced rather than silently dropped: a tactic here is an UNSUPPORTED claim, not a finding, and should not be treated as evidence."},"reciprocity_floor_applied":{"type":"boolean","description":"True when the verdict was floored to at least medium / 0.6 / flag_for_review because a separate factual classifier confirmed adult-minor romantic reciprocity - an adult and a minor declaring or reciprocating romantic love, directly or relayed through a third party (#189). A raise-only floor: it never lowers anything, and it exists because this implicit-outcome shape carries no tactic vocabulary and its model-side detection proved unstable across sittings (#292). Absent when the floor did not apply."},"consent_floor_applied":{"type":"boolean","description":"True when `recommended_action` was raised to `immediate_intervention` because the model reported an explicit refusal being overridden, or a threat to expose an intimate image or a confidence, in order to obtain compliance. This is a floor and never lowers an action. It exists because urgent routing on this endpoint is otherwise driven by the adult/child age gap, which is structurally absent when both participants are minors, leaving peer sexual coercion unable to reach the urgent band (#278). Absent when the floor did not apply."},"consent_violation":{"type":"string","enum":["explicit_refusal_overridden","threatened_exposure"],"description":"The factual finding behind `consent_floor_applied`, present only when one was reported. Deliberately describes what happened rather than who did it, so it is independent of the participants' ages."},"rationale":{"type":"string"},"risk_score":{"type":"number"},"recommended_action":{"type":"string","enum":["none","monitor","flag_for_review","block","immediate_intervention"],"description":"Routing key — a single stable token, safe to switch on. Ordered weakest to strongest: `none` (no signal), `monitor` (log, no human needed), `flag_for_review` (queue for a moderator), `block` (withhold or limit the content), `immediate_intervention` (crisis path). Human-readable guidance is in `action_detail`, not here."},"action_detail":{"type":"string","description":"Optional one-to-two-sentence explanation of `recommended_action`, intended for a moderator UI. Never branch on this field — it is free text and its wording is not stable across releases."},"message_analysis":{"type":"array","description":"Per-message risk breakdown showing how risk escalates across the conversation. Rows are emitted for the messages the model judged relevant, so a conversation may have fewer rows than messages: a message without a row carried no per-message finding, and is not the same as a row with risk_score 0. `message_index` is model-supplied; every flag on a row was checked against that message by a separate adjudication, and a flag the check refuted is moved to the row's `unsupported_flags` rather than left in `flags`.","items":{"type":"object","properties":{"message_index":{"type":"number","description":"1-based index of the message. The model names the message each row describes and the service re-keys a row whose name matches a different message, before any flag on it is checked."},"risk_score":{"type":"number","description":"Individual risk score (0.0-1.0). Set to 0 when every tactic the row claimed was withdrawn (see `unsupported_flags`); a row that claimed no tactic keeps its score."},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics detected in this message and supported by it"},"unsupported_flags":{"type":"array","items":{"type":"string"},"description":"Tactics the model claimed on this message that were withdrawn: refuted for this message by the adjudication, or withdrawn at conversation level (see `ungrounded_flags`). Not evidence. The summary carries a verification note when this is present."},"summary":{"type":"string","description":"Brief explanation of risk contribution. Prose for a reviewer; wording is not stable and may carry a bracketed verification note. Never branch on this field: use `flags` and `unsupported_flags`."}}}},"language":{"type":"string","description":"Language code used for analysis (auto-detected or provided)"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity: \"stable\" (en) or \"beta\" (all others)"},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"continuation_token":{"type":"string","description":"Privacy-first signed token carrying derived analysis state across calls. Pass back on the next /grooming request."},"continuation_expires_at":{"type":"string","description":"ISO 8601 expiry timestamp of the continuation_token."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How prior state was sourced for this call."},"trajectory_risk":{"type":"number","description":"Conversation-level risk 0-1, distinct from `risk_score` which scores only this message. Anchored on the highest severity seen so far and decaying slowly across benign turns, so a friendly message after an escalation does not reset it. Absent on the first turn. Derived from the signed token: no message content is stored to produce it."},"trajectory":{"type":"string","enum":["rising","stable","declining","none"],"description":"Direction of travel across the conversation so far."},"severity_series":{"type":"array","items":{"type":"number"},"description":"Per-turn severity, oldest first — the evidence behind `trajectory_risk`, so a moderator can see why a benign-looking message carries elevated conversation risk."},"analysis_status":{"type":"string","enum":["ok","engine_error","out_of_scope_adults"],"description":"Status of the analysis. `ok` is the default. `engine_error` means the analysis engine could not produce a verdict (malformed/adversarial input) and `recommended_action` is forced to `flag_for_review`. `out_of_scope_adults` means the caller provided 2+ adult ages (via `context.participant_age`, `context.child_age`, or per-message `sender_age`) and the call was short-circuited as out of scope for grooming — consider /safety/coercive-control or /safety/social-engineering for adult-adult dynamics."},"speaker_stance":{"type":"string","enum":["deploying","describing_third_party","unclear"],"description":"The model's own conclusion on whether the content is the speaker deploying a tactic in this exchange, or describing/reporting one performed by a third party (a disclosure, a survivor testimony, a bystander report) — independent of severity. Present when the model returns a recognised value AND the conversation carries at least one tactic, either in `flags` or in `ungrounded_flags`. A stance is a statement about a tactic, so it is omitted on a conversation that never had one, where the value is also unstable between runs. It is NOT omitted when every tactic was withdrawn: that is the shape of a disclosure, where a speaker claims tactics that an adjudication then finds are described rather than performed, and it is exactly the case where a platform needs to know this is a report so it can route to support rather than moderation."},"action_capped":{"type":"boolean","description":"True when `recommended_action` was capped below `block` because `speaker_stance` was `describing_third_party` — withholding a disclosure or a help-seeking report silences the person who needs to be heard. THIS CAP changes routing only and never touches `grooming_risk` or `risk_score`. Those fields can still be cleared to `none`/`0` on the same response by the separate `disclosure_cleared` mechanism, which is a different decision with its own conditions: see that field. Absent when no cap applied."},"action_capped_reason":{"type":"string","description":"Present only when action_capped is true."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only included for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}}}}}}}}}},"/api/v1/safety/unsafe":{"post":{"summary":"Detect KOSA-aligned harm signals in text","tags":["Safety"],"description":"Comprehensive safety analysis covering all KOSA harm categories: self-harm and suicidal ideation, eating disorders, substance use, depression/anxiety indicators, compulsive usage patterns, sexual content, violence, and hate speech. Returns categorized findings with age-calibrated severity scoring. This is the recommended STARTING endpoint for broad content moderation, not a complete safety layer on its own. **Pair it with `/safety/bullying`, or call `/api/v1/analyse/multi`.** Peer-to-peer harassment, exposure or \"fisha\" accounts, and image-based abuse of a named person are scored by `/safety/bullying`; grooming trajectories by `/safety/grooming`; extremist recruitment by `/safety/radicalisation`. This endpoint also reports `unsafe: false` where the sender is DESCRIBING a harm rather than being subject to it (survivor testimony, news, a lesson, a child relaying something seen elsewhere), which is deliberate and keeps protective speech out of your queue, but means a message another detector would flag can come back clear here. Measured on our published kids-online-slang set, every case in that shape was caught by a sibling detector. If you call only this endpoint you inherit that boundary.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The content to analyze"},"context":{"type":"object","properties":{"age_group":{"type":"string","description":"Age group (e.g., \"14-17\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."}}},"options":{"type":"object","properties":{"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Minimum severity to show crisis support resources (default: high). Critical always shows."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: skip generating `action_detail`, the moderator-guidance expansion of `recommended_action`. Lower latency and a smaller response; `rationale` (used for incident summaries) and the verdict itself (severity, categories, recommended_action, risk_score) are unchanged."},"flag_profanity":{"type":"boolean","description":"Additive, deterministic word-list flag for plain profanity/vulgarity. Adds `profanity` to the response; never affects `unsafe`, `severity`, `risk_score`, or `recommended_action`. Free — no extra credits. Not a harm classifier: does not cover slurs or hate speech, which the detector itself already handles with full context."},"flag_risk_terms":{"type":"boolean","description":"Additive, deterministic word-list flag for bare drug and violence terms (e.g. \"cocaine\", \"kill\"). Adds `risk_terms` to the response; never affects `unsafe`, `severity`, `risk_score`, or `recommended_action`. Free — no extra credits. Purely lexical: WILL fire on benign uses of an included term (\"kill the lights\") since it cannot read context the way the detector itself does — combine with the detector’s own verdict for a stricter policy, don’t use in isolation as a block decision."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"continuation_token":{"type":"string","description":"Opaque signed token from a previous /unsafe call. Carries derived analysis state — no message content is stored server-side."},"reset_conversation":{"type":"boolean","description":"Discard any continuation_token and start a fresh conversation."},"new_conversation":{"type":"boolean","description":"Cross into a new conversation WITHOUT discarding the sustained_hopelessness pattern carried in continuation_token — unlike reset_conversation, which discards everything. Requires continuation_token to also be set. Use this when a new conversation starts on your side (e.g. a new chat session days later) but you still want Tuteliq to notice if the same pattern keeps recurring across those sessions."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side message id for evidence pointers."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence over the account-level flag for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Lets you control incident logging per request, e.g. suppress logging for test traffic."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"unsafe":{"type":"boolean","description":"Unsafe-content signal observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_review` / `block` / `immediate_intervention`), which is a stable enum, over the bare presence of a signal. SCOPED TO THIS ENDPOINT: `false` means no KOSA-category signal was found HERE, not that the content is safe. It is also `false` where the sender is describing a harm rather than being subject to it. Peer harassment and image-based abuse of a named person are scored by `/safety/bullying`, so do not read `false` as a clearance without also calling that endpoint or `/api/v1/analyse/multi`."},"categories":{"type":"array","items":{"type":"string"}},"severity":{"type":"string","enum":["low","medium","high","critical"]},"confidence":{"type":"number"},"risk_score":{"type":"number"},"rationale":{"type":"string"},"recommended_action":{"type":"string","enum":["none","monitor","flag_for_review","block","immediate_intervention"],"description":"Routing key, weakest to strongest: `none`, `monitor`, `flag_for_review`, `block`, `immediate_intervention`. Safe to switch on."},"action_detail":{"type":"string","description":"Optional human-readable expansion of `recommended_action` for a moderator UI. Free text: do not branch on it."},"context_factors":{"type":"array","items":{"type":"string"},"description":"Named influences on the severity assessment, restricted to values that are safe to publish. Currently only `stated_test_frame`, set when the speaker themselves called this a test, demo or rehearsal. DESCRIPTIVE: the statement is still scored as if genuine, because presenting a disclosure as a test is also how a real one gets minimised, so this marks the frame for a reviewer rather than discounting it. Absent when nothing applies. ADVISORY: for a moderator UI and triage queues. Do not branch production logic on it; use `recommended_action`, which is a stable enum."},"language":{"type":"string","description":"Language code used for analysis (auto-detected or provided)"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity: \"stable\" (en) or \"beta\" (all others)"},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"continuation_token":{"type":"string","description":"Signed token carrying derived analysis state for the next call. Server stores nothing."},"continuation_expires_at":{"type":"string","description":"ISO 8601 expiry timestamp of the continuation_token."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How prior state was sourced."},"trajectory_risk":{"type":"number","description":"Conversation-level risk 0-1, distinct from `risk_score` which scores only this message. Anchored on the highest severity seen so far and decaying slowly across benign turns. Absent on the first turn. Derived from the signed token: no message content is stored to produce it."},"trajectory":{"type":"string","enum":["rising","stable","declining","none"],"description":"Direction of travel across the conversation so far."},"severity_series":{"type":"array","items":{"type":"number"},"description":"Per-turn severity, oldest first — the evidence behind `trajectory_risk`."},"sustained_hopelessness_conversations":{"type":"number","description":"Count of distinct conversations, within the last 30 days, that named the hopelessness_sustained marker at least once. Only present when new_conversation has been used to cross at least one conversation boundary. recommended_action is floored to at least flag_for_review once this reaches 3."},"escalation_capped":{"type":"boolean","description":"True when a critical-severity coded-term match corroborated this detection but the model's own independent read wasn't itself concerned enough to also corroborate escalation to immediate_intervention. Absent when no cap applied."},"escalation_capped_reason":{"type":"string","description":"Human-readable explanation of the cap. Present only when escalation_capped is true."},"third_party_concern":{"type":"boolean","description":"True when this detection rests on a child reporting, disclosing or reacting to a real risk involving somebody, rather than on anyone in the conversation enacting one -- a peer saying another child has self-harm marks, that a sibling is using again, that a suicide challenge is going round. Route these to a welfare/safeguarding queue rather than an intervention queue: the speaker is the person who spoke up, and the risk may belong to somebody who is not in the conversation. Set only when the verdict rests on that report: if the analysis independently called the content unsafe, this is absent and recommended_action carries the answer. This path itself raises neither severity nor action, but it is NOT a cap on the response -- always route on recommended_action, which can be anything up to immediate_intervention. Absent unless it applied."},"third_party_concern_reason":{"type":"string","description":"Human-readable explanation. Present only when third_party_concern is true."},"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text working against the harm it describes: teaching, moderating, counselling, reporting or asking for advice. Two outcomes. When nobody is at risk in the text and the speaker is describing someone else, nothing is detected and the action is `monitor`. When a person is at risk and a responsible adult in the text is already acting, an urgent action becomes `flag_for_review` with the severity and score brought down, and never lower. It never applies to a person speaking about their own risk, to a verdict supported by an acute marker, or when earlier turns are in play. Absent unless it applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"number_coincidence_cleared":{"type":"boolean","description":"True when an alert rested on a number that is also known as a code, and a separate reading found the number in ordinary use (an address, a count, a price, a room) with nothing else harmful in the content. The verdict was cleared. Absent when this did not happen."},"number_coincidence_cleared_reason":{"type":"string","description":"Human-readable explanation. Present only when number_coincidence_cleared is true."},"figurative_low_mood_capped":{"type":"boolean","description":"True when a review alert whose ONLY concern was low mood was lowered to `monitor` because a separate reading found the words were not meant literally: an exaggeration or an everyday expression about a task, a person or an event (\"I can't take this homework any more\"), a quotation, or something past and resolved. It applies only when the detector named low mood and nothing else, with no acute marker and no earlier turns in play; self-harm, a threat or any other category and it is never asked. The categories, severity and score are unchanged. It never clears to `none`. Absent unless it applied."},"figurative_low_mood_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when figurative_low_mood_capped is true."},"banter_capped":{"type":"boolean","description":"True when an alert that a rule for declared minors had raised was lowered ONE step because the text is banter between people who know each other: every strong word is an exaggeration about an everyday frustration (homework, a chore, a game), the other person answers in kind, nobody asks for it to stop or shows fear or hurt, nothing is kept from an adult beyond the trivial matter itself, and nobody says how they feel about themselves or their life. A raise to `flag_for_review` is lowered to `monitor`, with `unsafe` false. A raise to `immediate_intervention` is lowered to `flag_for_review` and no further, with everything else in the result unchanged, so a person still looks. It applies only when the analysis named no acute marker, its own action was lower than the one returned, and two separate readings agree; any other answer, or no answer, keeps the alert. `banter_capped_reason` says which step was taken. Absent when it did not apply."},"banter_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when banter_capped is true."},"no_child_shown_capped":{"type":"boolean","description":"True when the request declared no age group and a review alert was lowered to `monitor` because nothing in the message shows a child. It applies only when every category named is one that ordinary adult life is mistaken for (sexual content, risky behaviour, substance use), with no acute marker and no earlier turns in play, and a separate reading found the writer mentioning their own everyday life, with no sign of anyone under 18 and nobody hurt, pressured or in danger. A declared age group, a message about another person, or any other category, and it is never applied. The categories, severity and score are unchanged. It never clears to `none`. Absent unless it applied."},"no_child_shown_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when no_child_shown_capped is true."},"conversation_distress_surfaced":{"type":"boolean","description":"True when earlier turns were submitted, the analysis found nothing in the latest message, and a separate reading of the whole conversation found that someone expressed distress about their own life in an earlier turn (surfaced as `distress_context` at `monitor`) or said something about ending their life or hurting themselves (surfaced as `self_harm` for review). Only ever surfaces; never lowers a result. Absent otherwise."},"conversation_distress_surfaced_reason":{"type":"string","description":"Human-readable explanation. Present only when conversation_distress_surfaced is true."},"logistics_only_capped":{"type":"boolean","description":"True when the only concern named was that a location or an arrangement was given (an address, a place, a time), no acute marker was present, and a separate reading found practical detail for an ordinary purpose and nothing else in the message. A location is not unsafe on its own, so the verdict is cleared (`unsafe` false, `recommended_action` `none`). Absent when it did not apply."},"logistics_only_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when logistics_only_capped is true."},"profanity":{"type":["object","null"],"description":"Present only when options.flag_profanity (or the account-level default_flag_profanity setting) is true. Deterministic word-list result — additive, never affects unsafe/severity/risk_score/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"string"}}}},"risk_terms":{"type":["object","null"],"description":"Present only when options.flag_risk_terms (or the account-level default_flag_risk_terms setting) is true. Deterministic drug/violence word-list result — additive, never affects unsafe/severity/risk_score/recommended_action. Purely lexical: can fire on benign uses of an included term.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"category":{"type":"string","enum":["drug","violence"]}}}}}},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only included for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}}}}}}}}}},"/api/v1/analysis/emotions":{"post":{"summary":"Analyze emotional patterns and mental health indicators in conversation","tags":["Analysis"],"description":"Analyze a conversation for dominant emotions, sentiment trajectory (improving/stable/worsening), and early mental health risk indicators. Surfaces persistent patterns of sadness, anxiety, anger, or withdrawal that may indicate depression or declining wellbeing. Returns emotion scores, trend analysis, a human-readable summary, and recommended follow-up actions.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","items":{"type":"object","required":["sender","text"],"properties":{"sender":{"type":"string","description":"Sender identifier"},"text":{"type":"string","description":"Message text"}}},"description":"Chat history to analyze"},"context":{"type":"object","properties":{"age_group":{"type":"string","description":"Age group (e.g., \"11-13\")"}}},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier for correlation with your systems"},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Custom key-value pairs for additional context. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence over the account-level flag for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Lets you control incident logging per request, e.g. suppress logging for test traffic."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"dominant_emotions":{"type":"array","items":{"type":"string"}},"emotion_scores":{"type":"object","additionalProperties":{"type":"number"}},"trend":{"type":"string","enum":["improving","stable","worsening"]},"summary":{"type":"string"},"recommended_followup":{"type":"string"},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"external_id":{"type":"string","description":"Echo of provided external_id"},"metadata":{"type":"object","description":"Echo of provided metadata"}}}}}}}}},"/api/v1/guidance/action-plan":{"post":{"summary":"Generate age-appropriate action plan for a safety situation","tags":["Guidance"],"description":"Generate a step-by-step action plan tailored to the audience: child-friendly guidance (with reading-level calibration), parent advice, or platform trust & safety team procedures. Adjusts tone, vocabulary, and recommended steps based on the child's age and incident severity. Goes beyond detection to answer \"what should we do next?\"","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role","situation"],"properties":{"role":{"type":"string","enum":["child","parent","platform"],"description":"Target audience for the action plan"},"situation":{"type":"string","description":"Description of the situation"},"child_age":{"type":"number","description":"Age of the child"},"severity":{"type":"string","description":"Severity level"},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier for correlation with your systems"},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Custom key-value pairs for additional context. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence over the account-level flag for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Lets you control incident logging per request, e.g. suppress logging for test traffic."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"audience":{"type":"string"},"steps":{"type":"array","items":{"type":"string"}},"tone":{"type":"string"},"approx_reading_level":{"type":"string"},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"external_id":{"type":"string","description":"Echo of provided external_id"},"metadata":{"type":"object","description":"Echo of provided metadata"}}}}}}}}},"/api/v1/reports/incident":{"post":{"summary":"Generate professional incident report from conversation data","tags":["Reports"],"description":"Convert raw conversation messages into a structured incident report suitable for school counselors, moderators, or trust & safety teams. Returns a narrative summary, overall risk level, categorized findings, and recommended next steps. Accepts optional metadata (conversation ID, child age, timestamp range) for richer context.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messages"],"properties":{"messages":{"type":"array","items":{"type":"object","required":["sender","text"],"properties":{"sender":{"type":"string","description":"Sender identifier"},"text":{"type":"string","description":"Message text"}}},"description":"Messages involved in the incident"},"meta":{"type":"object","properties":{"conversation_id":{"type":"string","description":"Conversation identifier"},"child_age":{"type":"number","description":"Age of the child"},"timestamp_range":{"type":"array","items":{"type":"string"},"description":"Start and end timestamps"}}},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier for correlation with your systems"},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Custom key-value pairs for additional context. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence over the account-level flag for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Lets you control incident logging per request, e.g. suppress logging for test traffic."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"summary":{"type":"string"},"risk_level":{"type":"string"},"categories":{"type":"array","items":{"type":"string"}},"recommended_next_steps":{"type":"array","items":{"type":"string"}},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"external_id":{"type":"string","description":"Echo of provided external_id"},"metadata":{"type":"object","description":"Echo of provided metadata"}}}}}}}}},"/api/v1/policy/":{"get":{"summary":"Get current detection policy configuration","tags":["Policy"],"description":"Retrieve your active policy configuration including sensitivity thresholds, enabled categories, and custom moderation rules. Policies control how the safety endpoints evaluate and score content for your specific use case.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"config":{"type":"object","additionalProperties":true},"message":{"type":"string"}}}}}}}},"put":{"summary":"Update detection policy configuration","tags":["Policy"],"description":"Apply new policy settings to customize detection behavior. Changes take effect immediately for all subsequent API calls. Use to adjust sensitivity thresholds, enable/disable specific harm categories, or set custom moderation rules for your platform.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["config"],"properties":{"config":{"type":"object","additionalProperties":true,"description":"Policy configuration object"}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"config":{"type":"object","additionalProperties":true},"message":{"type":"string"}}}}}}}}},"/api/v1/batch/analyze":{"post":{"summary":"Analyze up to 50 items in a single request","tags":["Batch"],"description":"Submit multiple analysis tasks in one API call with optional parallel processing. Each item specifies its type (bullying, grooming, unsafe, or emotions) and data. Returns individual results for each item plus a summary with total/successful/failed counts and processing time. Ideal for content moderation backlogs, message queue processing, and bulk imports.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","maxItems":50,"items":{"type":"object","required":["id","type","data"],"properties":{"id":{"type":"string","description":"Unique identifier for this item"},"type":{"type":"string","enum":["bullying","grooming","unsafe","emotions","social_engineering","app_fraud","romance_scam","mule_recruitment","gambling_harm","coercive_control","vulnerability_exploitation","radicalisation"],"description":"Type of analysis to perform"},"data":{"type":"object","description":"Data to analyze (varies by type)","additionalProperties":true}}}},"parallel":{"type":"boolean","default":true,"description":"Process items in parallel (faster but uses more resources)"}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"success":{"type":"boolean"},"result":{"type":"object","additionalProperties":true},"error":{"type":"string"},"error_code":{"type":"string","description":"Error code for a failed item (e.g. ANALYSIS_6009 for a declared language that is not supported)."},"credits_used":{"type":"number"}}}},"summary":{"type":"object","properties":{"total":{"type":"number"},"successful":{"type":"number"},"failed":{"type":"number"},"processingTimeMs":{"type":"number"},"total_credits_used":{"type":"number"}}}}}}}}}}},"/api/v1/usage/summary":{"get":{"summary":"Get daily usage summary with quota status","tags":["Usage"],"description":"Returns total, successful, and failed request counts for a specific date (defaults to today), along with your tier limits. Paid tiers have no daily limit: `requestsPerDay` and `remainingToday` are -1, and the monthly allowance can be used at any pace. Use for dashboards and usage monitoring.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Date in YYYY-MM-DD format (defaults to today)"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"apiKeyId":{"type":"string"},"tier":{"type":"string"},"date":{"type":"string"},"usage":{"type":"object","properties":{"totalRequests":{"type":"number"},"successRequests":{"type":"number"},"errorRequests":{"type":"number"}}},"quota":{"type":"object","properties":{"requestsPerMinute":{"type":"number"},"requestsPerMonth":{"type":"number","description":"Monthly allowance of the tier. -1 means no limit."},"requestsPerDay":{"type":"number","description":"Always -1 for a paid tier: there is no daily limit. A sandbox key reports its daily cap."},"remainingToday":{"type":"number"}}}}}}}}}}},"/api/v1/usage/history":{"get":{"summary":"Get usage history over time","tags":["Usage"],"description":"Returns daily request counts (total, successful, failed) for the past N days (1–30, default 7). Use for trend analysis, usage charts, and identifying peak traffic periods.","parameters":[{"schema":{"type":"number","minimum":1,"maximum":30,"default":7},"in":"query","name":"days","required":false,"description":"Number of days of history (max 30)"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"api_key_id":{"type":"string"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"total_requests":{"type":"number"},"success_requests":{"type":"number"},"error_requests":{"type":"number"}}}}}}}}}}}},"/api/v1/usage/quota":{"get":{"summary":"Get real-time rate limit and quota status","tags":["Usage"],"description":"Returns your current rate limit status (requests this minute vs. limit) and the number of requests made today. Paid tiers have no daily limit: `limits.requestsPerDay` and `remaining.requestsToday` are -1, and the monthly allowance can be used at any pace. Use before making bulk requests to avoid hitting the per-minute limit.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"apiKeyId":{"type":"string"},"tier":{"type":"string"},"resetsInSeconds":{"type":"number","description":"Seconds until the per-minute rate-limit window resets"},"limits":{"type":"object","properties":{"requestsPerMinute":{"type":"number"},"requestsPerMonth":{"type":"number","description":"Monthly allowance of the tier. -1 means no limit."},"requestsPerDay":{"type":"number","description":"Always -1 for a paid tier: there is no daily limit. A sandbox key reports its daily cap."}}},"current":{"type":"object","properties":{"requestsThisMinute":{"type":"number"},"requestsToday":{"type":"number"}}},"remaining":{"type":"object","properties":{"requestsThisMinute":{"type":"number"},"requestsToday":{"type":"number"}}}}}}}}}}},"/api/v1/usage/by-tool":{"get":{"summary":"Get usage breakdown by tool and endpoint","tags":["Usage"],"description":"Returns the request counts of the API key making the call, broken down by analysis tool (bullying, grooming, unsafe, etc.) and endpoint path. Only that key's own requests are counted. Helps identify which safety features your integration uses most.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Date in YYYY-MM-DD format (defaults to today)"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"tools":{"type":"object","additionalProperties":{"type":"number"}},"endpoints":{"type":"object","additionalProperties":{"type":"number"}}}}}}}}}},"/api/v1/usage/monthly":{"get":{"summary":"Get monthly billing period usage and upgrade recommendations","tags":["Usage"],"description":"Returns your current billing period usage (calls used vs. limit), tier details, rate limits, and days remaining. Includes smart upgrade recommendations when you approach 80% usage or have fewer than 200 calls remaining. Links to dashboard, pricing, and credit purchase pages.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"tier":{"type":"string"},"tier_display_name":{"type":"string"},"billing":{"type":"object","properties":{"current_period_start":{"type":"string"},"current_period_end":{"type":"string"},"days_remaining":{"type":"number"}}},"usage":{"type":"object","properties":{"used":{"type":"number"},"limit":{"type":"number"},"remaining":{"type":"number"},"percent_used":{"type":"number"}}},"rate_limit":{"type":"object","properties":{"requests_per_minute":{"type":"number"}}},"recommendations":{"type":["object","null"],"properties":{"should_upgrade":{"type":"boolean"},"reason":{"type":"string"},"suggested_tier":{"type":"string"},"upgrade_url":{"type":"string"}}},"links":{"type":"object","properties":{"dashboard":{"type":"string"},"pricing":{"type":"string"},"buy_credits":{"type":"string"}}}}}}}}}}},"/api/v1/webhooks/":{"get":{"summary":"List all webhooks","tags":["Webhooks"],"description":"Returns all webhooks configured for the authenticated user","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"failure_count":{"type":"number"},"last_triggered_at":{"type":["string","null"]},"last_error":{"type":["string","null"]},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}}}}}},"post":{"summary":"Create a webhook","tags":["Webhooks"],"description":"Create a new webhook endpoint for receiving notifications","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","url","events"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"url":{"type":"string","format":"uri","pattern":"^https://"},"events":{"type":"array","items":{"type":"string","enum":["incident.critical","incident.high","grooming.detected","self_harm.detected","bullying.severe","verification.completed","minor.detected","csam.detected","csam.critical"]},"minItems":1,"maxItems":5},"headers":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"secret":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"created_at":{"type":"string"}}}}}}}}},"/api/v1/webhooks/{webhookId}":{"put":{"summary":"Update a webhook","tags":["Webhooks"],"description":"Update an existing webhook configuration","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"url":{"type":"string","format":"uri","pattern":"^https://"},"events":{"type":"array","items":{"type":"string","enum":["incident.critical","incident.high","grooming.detected","self_harm.detected","bullying.severe","verification.completed","minor.detected","csam.detected","csam.critical"]},"minItems":1,"maxItems":5},"is_active":{"type":"boolean"},"headers":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"webhookId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"is_active":{"type":"boolean"},"updated_at":{"type":"string"}}}}}}}},"delete":{"summary":"Delete a webhook","tags":["Webhooks"],"description":"Permanently delete a webhook","parameters":[{"schema":{"type":"string"},"in":"path","name":"webhookId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}}}}},"/api/v1/webhooks/test":{"post":{"summary":"Test a webhook","tags":["Webhooks"],"description":"Send a test payload to verify webhook configuration","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["webhook_id"],"properties":{"webhook_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"status_code":{"type":"number"},"latency_ms":{"type":"number"},"error":{"type":"string"}}}}}}}}},"/api/v1/webhooks/{webhookId}/regenerate-secret":{"post":{"summary":"Regenerate webhook secret","tags":["Webhooks"],"description":"Generate a new signing secret for the webhook","parameters":[{"schema":{"type":"string"},"in":"path","name":"webhookId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"secret":{"type":"string"}}}}}}}}},"/api/v1/safety/voice":{"post":{"summary":"Analyze voice/audio for safety concerns with transcription","tags":["Safety"],"description":"Upload an audio file (mp3, wav, m4a, ogg, flac, webm, mp4 — max 25MB) for automatic transcription followed by full safety analysis on the transcript. Returns timestamped segments, per-category analysis (bullying, grooming, unsafe, emotions), and an overall risk score. Ideal for monitoring voice chats, audio messages, and recorded conversations.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string"},"transcription":{"type":"object","properties":{"text":{"type":"string"},"language":{"type":"string"},"duration":{"type":"number"},"segments":{"type":"array","items":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"},"text":{"type":"string"}}}}}},"analysis":{"type":"object","additionalProperties":true},"overall_risk_score":{"type":"number"},"overall_severity":{"type":"string","enum":["none","low","medium","high","critical"]},"detected":{"type":"boolean","description":"A concern was observed at ANY severity, including monitor-only cases. For production branching prefer `recommended_action`."},"confidence":{"type":"number","description":"Confidence in the overall verdict (0-1)."},"recommended_action":{"type":"string","enum":["none","monitor","flag_for_review","block","immediate_intervention"],"description":"Routing key, weakest to strongest: `none`, `monitor`, `flag_for_review`, `block`, `immediate_intervention`. Safe to switch on."},"action_detail":{"type":"string","description":"Human-readable expansion of `recommended_action` for a moderator UI. Free text: do not branch on it."},"rationale":{"type":"string","description":"Why this verdict was reached. Built from category and severity labels only; never contains the transcript or media description."},"language":{"type":"string","description":"Detected or specified language code. 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh)."},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity: \"stable\" (en) or \"beta\" (all others)"},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"external_id":{"type":"string"},"incident_moderation_enabled":{"type":"string","description":"Per-call override of account incident logging: \"true\" forces persistence, \"false\" suppresses it. Omit for account default."},"customer_id":{"type":"string"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"}}}}}}}}},"/api/v1/safety/image":{"post":{"summary":"Analyze image for visual and textual safety concerns","tags":["Safety"],"description":"Upload an image (png, jpg, gif, webp — max 10MB) for dual-layer analysis: Vision AI classifies visual content (nudity, violence, weapons, drugs, self-harm imagery) while OCR extracts embedded text for separate safety analysis. Returns visual categories, extracted text, face detection, and combined overall risk score. Covers screenshots, memes, shared photos, and profile images.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string"},"vision":{"type":"object","properties":{"extracted_text":{"type":"string"},"visual_categories":{"type":"array","items":{"type":"string"}},"visual_severity":{"type":"string"},"visual_confidence":{"type":"number"},"visual_description":{"type":"string"},"contains_text":{"type":"boolean"},"contains_faces":{"type":"boolean"}}},"text_analysis":{"type":"object","additionalProperties":true},"overall_risk_score":{"type":"number"},"overall_severity":{"type":"string","enum":["none","low","medium","high","critical"]},"detected":{"type":"boolean","description":"A concern was observed at ANY severity, including monitor-only cases. For production branching prefer `recommended_action`."},"confidence":{"type":"number","description":"Confidence in the overall verdict (0-1)."},"recommended_action":{"type":"string","enum":["none","monitor","flag_for_review","block","immediate_intervention"],"description":"Routing key, weakest to strongest: `none`, `monitor`, `flag_for_review`, `block`, `immediate_intervention`. Derived from the stronger of the visual verdict and the OCR text verdict, so harmful text in a visually benign image still routes correctly. Safe to switch on."},"action_detail":{"type":"string","description":"Human-readable expansion of `recommended_action` for a moderator UI. Free text: do not branch on it."},"rationale":{"type":"string","description":"Why this verdict was reached. Built from category and severity labels only; never contains the image description or its extracted text."},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"external_id":{"type":"string"},"incident_moderation_enabled":{"type":"string","description":"Per-call override of account incident logging: \"true\" forces persistence, \"false\" suppresses it. Omit for account default."},"customer_id":{"type":"string"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"profanity":{"type":["object","null"],"description":"Present only when the flag_profanity form field (or the account-level default_flag_profanity setting) is true, and only when the image contained OCR text. Deterministic word-list result over the extracted text — additive, never affects vision/text_analysis/overall_severity/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"string"}}}},"risk_terms":{"type":["object","null"],"description":"Present only when the flag_risk_terms form field (or the account-level default_flag_risk_terms setting) is true, and only when the image contained OCR text. Deterministic drug/violence word-list result over the extracted text — additive, never affects vision/text_analysis/overall_severity/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"category":{"type":"string","enum":["drug","violence"]}}}}}}}}}}}}}},"/api/v1/safety/video":{"post":{"summary":"Analyze video for visual safety concerns across extracted frames","tags":["Safety"],"description":"Upload a video file (mp4, webm, quicktime, avi — max 100MB, max 10 min). Extracts key frames at even intervals and runs vision analysis on each frame. Returns per-frame results, flagged timestamps, and an aggregated overall risk score. Ideal for moderating uploaded video content, screen recordings, and video messages.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string"},"frames_analyzed":{"type":"number"},"duration_seconds":{"type":"number"},"frame_results":{"type":"array","items":{"type":"object","properties":{"frame_index":{"type":"number"},"timestamp_s":{"type":"number"},"vision":{"type":"object","additionalProperties":true},"risk_score":{"type":"number"},"severity":{"type":"string"}}}},"categories":{"type":"array","items":{"type":"string"},"description":"Visual harm categories across every flagged frame, deduped."},"overall_risk_score":{"type":"number"},"overall_severity":{"type":"string","enum":["none","low","medium","high","critical"]},"detected":{"type":"boolean","description":"A concern was observed at ANY severity, including monitor-only cases. For production branching prefer `recommended_action`."},"confidence":{"type":"number","description":"Confidence in the overall verdict (0-1), from the frame that drove it."},"recommended_action":{"type":"string","enum":["none","monitor","flag_for_review","block","immediate_intervention"],"description":"Routing key, weakest to strongest: `none`, `monitor`, `flag_for_review`, `block`, `immediate_intervention`. Safe to switch on. A video whose frames could not be decoded returns `flag_for_review`, never `none`."},"action_detail":{"type":"string","description":"Human-readable expansion of `recommended_action` for a moderator UI. Free text: do not branch on it."},"rationale":{"type":"string","description":"Why this verdict was reached. Built from category and severity labels only; never contains the frame descriptions."},"flagged_timestamps":{"type":"array","items":{"type":"object","properties":{"timestamp_s":{"type":"number"},"reason":{"type":"string"},"severity":{"type":"string"}}}},"credits_used":{"type":"number","description":"Number of credits consumed by this request"},"external_id":{"type":"string"},"incident_moderation_enabled":{"type":"string","description":"Per-call override of account incident logging: \"true\" forces persistence, \"false\" suppresses it. Omit for account default."},"customer_id":{"type":"string"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"profanity":{"type":["object","null"],"description":"Present only when the flag_profanity form field (or the account-level default_flag_profanity setting) is true, and only when at least one frame contained OCR text. Deterministic word-list result over the OCR text aggregated across frames — additive, never affects frame_results/overall_severity/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"string"}}}},"risk_terms":{"type":["object","null"],"description":"Present only when the flag_risk_terms form field (or the account-level default_flag_risk_terms setting) is true, and only when at least one frame contained OCR text. Deterministic drug/violence word-list result over the OCR text aggregated across frames — additive, never affects frame_results/overall_severity/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"category":{"type":"string","enum":["drug","violence"]}}}}}}}}}}}}}},"/api/v1/fraud/social-engineering":{"post":{"summary":"Detect social engineering tactics in text","tags":["Fraud"],"description":"Analyzes text content for social engineering manipulation tactics including authority impersonation, urgency fabrication, pretexting, information elicitation, trust exploitation, fear induction, and quid pro quo patterns.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"third_party_report_surfaced":{"type":"boolean","description":"True when the analysis itself found no social engineering in the content, and the speaker tells of contact that came to them or to a specific person they know, asking for money, details, a code or access, with that person going along with it now (a relative on the phone with a caller, the speaker about to enter details on a link they were sent). The result is `flag_for_review`: route it to protection. It is a report, never a finding against the person writing."},"third_party_report_surfaced_reason":{"type":"string","description":"Human-readable explanation. Present only when third_party_report_surfaced is true."},"access_request_raised":{"type":"boolean","description":"True when the analysis itself found no social engineering in the content, and the writer asks to be given something that itself opens access (a door, gate or alarm code, a password or login, where a key is kept, or when a person or a home is unattended) while giving as the reason a role or connection the message cannot confirm (new on the team, covering for someone, sent by a named person, from a company or committee). The result is `flag_for_review` and never `block`: the asker may be exactly who they say, and who is asking should be checked before anyone answers."},"access_request_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when access_request_raised is true."},"lone_message_block_capped":{"type":"boolean","description":"True when a single message submitted with no earlier turns asks nothing of its reader, and a `block` was therefore brought down to `flag_for_review`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied when earlier turns were submitted. Absent otherwise."},"lone_message_block_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when lone_message_block_capped is true."},"only_confirmation_asked_capped":{"type":"boolean","description":"True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), the contact asks only to confirm facts it needs, such as yes or no, a date of birth, an address or the last digits of a card, and asks for no code, password, payment, access or move to another channel. A result at review or above is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, or a result a separate reading raised. Absent otherwise."},"only_confirmation_asked_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when only_confirmation_asked_capped is true."}}}}}}}}},"/api/v1/fraud/app-fraud":{"post":{"summary":"Detect app fraud and financial scam patterns","tags":["Fraud"],"description":"Analyzes text content for app fraud indicators including payment coercion, account takeover, safe account scams, invoice fraud, impersonation, investment pressure, and emotional financial exploitation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"target_report":{"type":"boolean","description":"True when the writer is telling of a fraud that is still taking money from them or from a specific person they know: they have paid, deposited, traded or given payment details to someone or something they know only online, and more is being asked or taken. `recommended_action` is `flag_for_review`: route it to protection. It is a report, never a finding against the person writing."},"target_report_reason":{"type":"string","description":"Human-readable explanation. Present only when target_report is true."},"target_report_action_was":{"type":"string","enum":["block"],"description":"Present only when the analysis had answered `block` and the result was made a review because the writer is the person being defrauded, not the person doing it. Level, score and categories are unchanged."}}}}}}}}},"/api/v1/fraud/romance-scam":{"post":{"summary":"Detect romance scam indicators in conversation","tags":["Fraud"],"description":"Analyzes text and conversation history for romance scam patterns including love bombing, isolation, fabricated crises, investment lures, identity deception, financial grooming, guilt manipulation, and crypto redirect tactics.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"speaker_stance":{"type":"string","enum":["deploying","describing_third_party","unclear"],"description":"Whether the speaker is running this on the listener, or recounting, reporting, warning about or investigating one run by someone else. A victim disclosing what was done to them is `describing_third_party`, even though the disclosure necessarily repeats the perpetrator's own vocabulary. DESCRIPTIVE: this never changes `detected`, `severity`, `level` or `recommended_action`, so read it before acting on them, or a victim report is actioned exactly like the fraud it reports."}}}}}}}}},"/api/v1/fraud/mule-recruitment":{"post":{"summary":"Detect money mule recruitment attempts","tags":["Fraud"],"description":"Analyzes text content for money mule recruitment indicators including easy money promises, account access requests, multi-level recruitment, risk minimisation, secrecy demands, social pressure, and coercion.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"speaker_stance":{"type":"string","enum":["deploying","describing_third_party","unclear"],"description":"The model's own conclusion on whether the speaker is recruiting the listener into moving money, or asking about/investigating an arrangement run by someone else (e.g. a bank fraud investigator interviewing a suspected victim). Feeds into recommended_action -- see action_capped below."},"action_capped":{"type":"boolean","description":"True when speaker_stance capped a suppressive recommended_action (e.g. block) below itself, because the model identified the speaker as describing/countering a third party's conduct rather than deploying it themselves."},"action_capped_reason":{"type":"string","description":"Human-readable explanation of the action_capped adjustment, present only when action_capped is true."},"severity_capped":{"type":"boolean","description":"True when speaker_stance capped severity/risk_score/level at a ceiling (not to zero) for the same reason as action_capped -- a genuine third-party disclosure or exposure report still reaches human review at the ceiling; only a purely educational/protective description is prevented from reading as comparable to a real attack."},"severity_capped_reason":{"type":"string","description":"Human-readable explanation of the severity_capped adjustment, present only when severity_capped is true."}}}}}}}}},"/api/v1/safety/gambling-harm":{"post":{"summary":"Detect gambling harm indicators","tags":["Safety Extended"],"description":"Analyzes text content for gambling harm indicators including chasing losses, desperation, borrowing, loss of control, relationship impact, financial harm, and denial patterns.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}}}}}}}}}},"/api/v1/safety/coercive-control":{"post":{"summary":"Detect coercive control patterns","tags":["Safety Extended"],"description":"Analyzes text and conversation history for coercive control patterns including monitoring, isolation, financial control, threats, degradation, micromanagement, gaslighting, and child weaponisation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was. `severity`, `level` and `risk_score` are never altered by it."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"speaker_role":{"type":"string","enum":["enacting","experiencing","reporting_other","unclear"],"description":"The speaker's relationship to the conduct described. `speaker_stance` cannot separate a victim from a professional reporting on someone else, because both are `describing_third_party`; this can. `enacting` = the speaker carries the conduct out, `experiencing` = the conduct is directed at them, `reporting_other` = neither party."},"reporter_capped":{"type":"boolean","description":"True when `recommended_action` was lowered from `immediate_intervention` to `flag_for_review` because the speaker is reporting on a third party rather than involved in the conduct. Requires both `speaker_role: reporting_other` AND no first-person self-reference in the text. `severity`, `level` and `risk_score` are NOT affected and the action never drops below `flag_for_review`, so the case still reaches a human queue at full weight. Absent when no cap applied."},"soft_evidence_capped":{"type":"boolean","description":"True when `recommended_action` was lowered from `flag_for_review` to `monitor` because the content is in the medium band and names only directive or checking-in behaviour (a rule, a curfew, a request for a location, a disagreement), with no threat, isolation, financial control, surveillance tooling or sign of impact on the person. `detected`, `severity`, `level` and `risk_score` are NOT changed, and the case stays visible at `monitor` so a pattern can still build across a conversation. Never applied at or above 0.6 severity, never to an urgent action. Absent when it did not apply."},"soft_evidence_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when soft_evidence_capped is true."},"document_safekeeping_capped":{"type":"boolean","description":"True when a review alert was lowered to `monitor` because a separate reading found a message about documents held for safekeeping or a practical purpose (a parent carrying the family's passports on a journey, papers kept together at home), with nothing saying the owner wants them and cannot have them, and nobody restricted, threatened or afraid. What defines this harm is documents taken or kept against a person's will. It never applies to an urgent action, to a severity of 0.6 or more, or when anything harder is named beside it. `detected` is false and the result stays visible at `monitor`."},"document_safekeeping_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when document_safekeeping_capped is true."},"ordinary_discipline_capped":{"type":"boolean","description":"True when a review alert was lowered to `monitor` because a separate reading found a parent or carer giving a child a rule or a consequence that has a stated reason and an end (no phone tonight, staying in until the weekend, a console put away until the homework is handed in), with no basic need and no contact with others taken away, nobody frightened or degraded, and no threat or violence. What defines this harm is a pattern that leaves a child unsafe, alone, deprived or afraid. It never applies to an urgent action, to a critical level, or when a threat, degradation, money or anything harder is named. `detected` is false and the result stays visible at `monitor`."},"ordinary_discipline_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_discipline_capped is true."},"speaker_stance":{"type":"string","enum":["deploying","describing_third_party","unclear"],"description":"The model's own conclusion on whether the speaker is exercising a controlling tactic themselves or asking about/describing/reporting one performed by someone else -- e.g. a danger-assessment screening question from an advocate or counsellor. Feeds into severity/action -- see action_capped/severity_capped below."},"action_capped":{"type":"boolean","description":"True when speaker_stance capped a suppressive recommended_action (e.g. block) below itself, because the model identified the speaker as describing/countering a third party's conduct rather than deploying it themselves."},"action_capped_reason":{"type":"string","description":"Human-readable explanation of the action_capped adjustment, present only when action_capped is true."},"severity_capped":{"type":"boolean","description":"True when speaker_stance capped severity/risk_score/level at a ceiling (not to zero) for the same reason as action_capped -- a genuine third-party disclosure or exposure report still reaches human review at the ceiling; only a purely educational/protective description is prevented from reading as comparable to a real attack."},"severity_capped_reason":{"type":"string","description":"Human-readable explanation of the severity_capped adjustment, present only when severity_capped is true."}}}}}}}}},"/api/v1/safety/vulnerability-exploitation":{"post":{"summary":"Detect exploitation of vulnerable individuals","tags":["Safety Extended"],"description":"Analyzes text for vulnerability exploitation including age targeting, emotional exploitation, cognitive exploitation, situational vulnerability, trust abuse, and information asymmetry. Returns additional cross-endpoint modifier and vulnerability profile.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"cross_endpoint_modifier":{"type":"number","description":"Vulnerability modifier (1.0-2.0) to amplify other endpoint severity scores"},"vulnerability_profile":{"type":"object","properties":{"emotional_vulnerability":{"type":"number","description":"Emotional vulnerability score (0.0-1.0)"},"cognitive_vulnerability":{"type":"number","description":"Cognitive vulnerability score (0.0-1.0)"},"situational_vulnerability":{"type":"number","description":"Situational vulnerability score (0.0-1.0)"},"social_vulnerability":{"type":"number","description":"Social vulnerability score (0.0-1.0)"}}},"speaker_stance":{"type":"string","enum":["deploying","describing_third_party","unclear"],"description":"The model's own conclusion on whether the speaker is exploiting the listener's vulnerability themselves, or warning them about someone else who might (e.g. a bank's fraud-prevention agent doing a protective check-in). Feeds into recommended_action -- see action_capped below."},"resource_direction_cleared":{"type":"boolean","description":"True when the detection was cleared because nothing was sought from the speaker and the only tactics were TRUST_ABUSE / INFO_ASYMMETRY, which describe the shape of a relationship rather than evidencing an act (#267). An authority relationship plus money was previously enough to fire on, for example, a parent moving their own child's savings. Absent when the gate did not fire."},"third_party_report_cleared":{"type":"boolean","description":"True when the detection was cleared because the model committed both that the speaker is DESCRIBING exploitation rather than deploying it, and that the person it targets is an absent third party (#267) — the case-note/referral/briefing register, e.g. a social worker documenting suspected financial abuse of a client. Never fires when the described target is a participant, so a victim disclosing their own exploitation, or a live target being groomed through hypothetical phrasing, is unaffected. Absent when the gate did not fire."},"third_party_report_surfaced":{"type":"boolean","description":"True when the speaker is describing a specific person, not in this conversation, who is being exploited now (a relative, a friend, a neighbour), and nothing in the content shows that a protective process already has it. The detection is kept and is at least `flag_for_review`: route it to protection. It is a report, never a finding against the person reporting it, and `block` is never returned on it. A professional case note or referral, an incident that is over, and general advice or news are cleared instead (see `third_party_report_cleared`). Absent unless it applied."},"third_party_report_surfaced_reason":{"type":"string","description":"Human-readable explanation. Present only when third_party_report_surfaced is true."},"action_capped":{"type":"boolean","description":"True when speaker_stance capped a suppressive recommended_action (e.g. block) below itself, because the model identified the speaker as describing/countering a third party's conduct rather than deploying it themselves."},"action_capped_reason":{"type":"string","description":"Human-readable explanation of the action_capped adjustment, present only when action_capped is true."},"severity_capped":{"type":"boolean","description":"True when speaker_stance capped severity/risk_score/level at a ceiling (not to zero) for the same reason as action_capped -- a genuine third-party disclosure or exposure report still reaches human review at the ceiling; only a purely educational/protective description is prevented from reading as comparable to a real attack."},"severity_capped_reason":{"type":"string","description":"Human-readable explanation of the severity_capped adjustment, present only when severity_capped is true."}}}}}}}}},"/api/v1/safety/radicalisation":{"post":{"summary":"Detect radicalisation and extremism indicators","tags":["Safety Extended"],"description":"Analyzes text and conversation history for radicalisation indicators including ideological seeding, identity exploitation, in-group bonding, out-group dehumanisation, grievance amplification, violence normalisation, operational planning, and isolation from moderating influences.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: teaching about it, moderating or removing it, counselling, researching or reporting on it, arguing against it, or raising a concern about someone. Two outcomes. When nobody is at risk in the text and the detector itself read the speaker as describing someone else, the result is nothing detected at `monitor`. Otherwise, including when a person is at risk and a responsible adult in the text is already acting on it, an urgent action becomes `flag_for_review` and the finding is unchanged: a person still sees it, and it is no longer routed as an emergency or as an action against the writer. It never applies when a person is at risk and nobody responsible is acting, when anyone in the text is promoting the harm, when there is any plan or threat of violence, or when operational planning or a call to violence was detected. Absent unless it applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"priority_review":{"type":"boolean","description":"True when high-risk radicalisation content involving a declared minor was found. `recommended_action` is `flag_for_review`; this field marks the case as one to review first, since it may need further escalation once a person has looked. Absent otherwise."},"priority_review_reason":{"type":"string","description":"Human-readable explanation. Present only when priority_review is true."},"game_play_capped":{"type":"boolean","description":"True when an alert was lowered to `monitor` because a separate reading found that everything in the text happens inside a game, a story or a sport: every target is a character, a team, a clan or a place inside it, no real person or group of real people is named or described, and nobody steps outside the play. Absent otherwise."},"game_play_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when game_play_capped is true."},"speaker_stance":{"type":"string","enum":["deploying","describing_third_party","unclear"],"description":"The model's own conclusion on whether the speaker is expressing extremist content themselves or reporting/describing someone else's (e.g. a worried child asking about content sent to them). Now feeds into severity/action -- see action_capped/severity_capped below."},"exposure_target":{"type":"string","enum":["self_targeted","bystander","unclear"],"description":"Present only when the educational minor-floor predicate fired: whether the reporting speaker is the RECIPIENT of the extremist content they describe (self_targeted - a classmate sending it to them, their own group chat) or a true bystander (a stranger's post, an unrelated third party). bystander exempts the artificial 0.65-0.80 educational floor and nothing else; the model's own verdict and the general minor floor stand. Fails closed: any error or unclear answer keeps the floor (#158)."},"action_capped":{"type":"boolean","description":"True when speaker_stance capped a suppressive recommended_action (e.g. block) below itself, because the model identified the speaker as describing/countering a third party's conduct rather than deploying it themselves."},"action_capped_reason":{"type":"string","description":"Human-readable explanation of the action_capped adjustment, present only when action_capped is true."},"severity_capped":{"type":"boolean","description":"True when speaker_stance capped severity/risk_score/level at a ceiling (not to zero) for the same reason as action_capped -- a genuine third-party disclosure or exposure report still reaches human review at the ceiling; only a purely educational/protective description is prevented from reading as comparable to a real attack."},"severity_capped_reason":{"type":"string","description":"Human-readable explanation of the severity_capped adjustment, present only when severity_capped is true."},"number_coincidence_cleared":{"type":"boolean","description":"True when an alert rested on a number that is also known as a code, and a separate reading found the number in ordinary use (an address, a count, a price, a room) with nothing else harmful in the content. The verdict was cleared. Absent when this did not happen."},"number_coincidence_cleared_reason":{"type":"string","description":"Human-readable explanation. Present only when number_coincidence_cleared is true."}}}}}}}}},"/api/v1/safety/distress-signals":{"post":{"summary":"Detect linguistic distress-signal patterns (pre-vulnerability indicators)","tags":["Safety Extended"],"description":"Analyses text for the presence of linguistic patterns historically associated with distress (loneliness expressions, isolation language, hopelessness phrases, trust-seeking openers, withdrawal references, family-conflict mentions). Returns pattern detections and, where applicable, downstream exploitation-risk assessment (grooming, sextortion, trafficking, radicalisation). Does NOT infer the author’s inner emotional state. Intended for child-safety contexts as a safety component (EU AI Act Art 5(1)(f) safety exception); not intended for workplace or general education monitoring.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."},"author_role":{"type":"string","enum":["subject","responder"],"description":"Who wrote the `text` being analysed, relative to the distress. \"subject\" = the person whose wellbeing is in question (the help-seeker, child, or user). \"responder\" = someone supporting or assessing them (a crisis counsellor, teacher, parent, social worker). Optional; when set, the response echoes `distress_subject` so a reviewer knows which party the detected signals belong to. Does NOT change severity or recommended_action -- a responder's question inside a genuine crisis is still a genuine crisis."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"distress_reading":{"type":"string","enum":["literal_present","about_other","past_resolved","figurative","quoted","none"],"description":"How the words are being used, from a separate reading taken when the verdict would otherwise alert. `literal_present` = someone is expressing their own present distress and means it. `about_other` = the distress belongs to someone who is not speaking (a friend, a relative, a professional describing or responding to it). `past_resolved` = the speaker describes distress they have come through. `figurative` = exaggeration, idiom or a joke about something ordinary (\"this homework is killing me\", \"kill me now\" about a game). `quoted` = a lyric, a quotation, fiction, news or teaching material. `none` = nothing about distress. Absent when no reading was taken."},"reading_capped":{"type":"boolean","description":"True when `recommended_action` was lowered because `distress_reading` was `figurative`, `quoted`, `past_resolved` or `none`: `flag_for_review` is lowered to `monitor`, and `block` and `immediate_intervention` are lowered to `flag_for_review` and no further, so a case routed above review is still seen by a person. `detected`, `severity`, `level` and `risk_score` are NOT changed, and the case always stays visible. Never applied when the reading is `literal_present` or `about_other`, or when no reading could be taken. Absent when it did not apply."},"reading_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when reading_capped is true."},"ordinary_day_capped":{"type":"boolean","description":"True when a `flag_for_review` was lowered to `monitor` because the message mentions an ordinary part of life and nothing more (parents arguing, giving up a hobby, a secret that is good or ordinary news). It applies only when every pattern named is family conflict, withdrawal or trust seeking, the action is review and no higher, there are no earlier turns in play, and a second separate reading found nothing more than an everyday matter and nobody else acting on the writer. `detected`, the categories, the severity and the score are unchanged. It never clears to `none`. Absent unless it applied."},"ordinary_day_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_day_capped is true."},"support_suggested":{"type":"boolean","description":"True when the analysis suggested a supportive check-in and nothing more: a low mood being met with comfort, for example. `recommended_action` is `monitor` in that case, so nothing here asks a moderator to act; a product that offers wellbeing prompts can use this field to offer one. It is never present beside `flag_for_review` or an urgent action, and it changes nothing else in the result. Absent otherwise."},"distress_subject":{"type":"string","enum":["speaker","other_party"],"description":"WHOSE distress the detected signals describe. \"speaker\" = the author of the analysed message is expressing their own distress. \"other_party\" = the author is supporting or assessing someone else who is in distress. Derived DETERMINISTICALLY from the `context.author_role` you supply -- not inferred by the model -- and omitted entirely when you do not supply it. Attribution only: this never changes severity or recommended_action."}}}}}}}}},"/api/v1/safety/tfgbv":{"post":{"summary":"Detect tech-facilitated gender-based violence","tags":["Safety Extended"],"description":"Analyzes text for tech-facilitated gender-based violence (TFGBV) patterns including image-based abuse, cyber stalking, online harassment, doxing, impersonation, outing, post-separation abuse, sextortion, digital coercion, sexualised deepfakes, and gendered hate speech. Returns TFGBV amplifier assessment and intersectionality flags.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when a separate reading lowered an alert to `monitor`. Two cases. A review alert on a text that recounts or reports this harm as something that is over, with nobody at risk now and nothing still standing. And, where your platform declared a `context.conversation_type` that names the responsible party in the conversation, a person at risk whom that party is already handling, with nothing still standing (no threat, no demand, no images still up or still held to be shared). Anything still standing keeps the alert in every setting. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"willing_exchange_capped":{"type":"boolean","description":"True when a review alert was lowered to `monitor` because a separate reading found a message about something two people share willingly: no pressure, nothing done without the other person, no hostility, and no sign of a minor. What defines this harm is the absence of consent. It never applies when the declared age group is, or includes, people under 18, and never to anything stronger than `flag_for_review` or to sextortion, a deepfake, stalking, doxing or outing. It never clears to `none`, so a conversation that turns stays visible. Absent unless it applied."},"willing_exchange_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when willing_exchange_capped is true."},"speaker_role":{"type":"string","enum":["enacting","experiencing","reporting_other","self_directed","unclear"],"description":"The speaker's relationship to the conduct described, derived from the content by a separate classifier so it cannot move the verdict. `enacting` = the speaker is carrying the abuse out. `experiencing` = the abuse is directed at them, which is what a victim disclosure looks like. `reporting_other` = a bystander, support worker or professional describing someone else's conduct. `self_directed` = the speaker refers only to themselves. `unclear` = cannot be determined. This is the ONLY field that separates a victim from a perpetrator on this endpoint: `detected`, `severity` and `recommended_action` are the same for both, because the abuse being described is equally real either way. Read it before you action the sender, or you will sanction the person who reported the abuse. DESCRIPTIVE ONLY: it never changes `detected`, `severity`, `risk_score` or `recommended_action`, and it is derived independently of the caller-declared `conversation_type`. Same vocabulary as `speaker_role` on /safety/bullying."}}}}}}}}},"/api/v1/analyse/multi":{"post":{"summary":"Fan-out analysis across multiple detection endpoints","tags":["Analyse"],"description":"Submit text to multiple detection endpoints in a single request. All endpoints run in parallel via Promise.allSettled. If vulnerability-exploitation is included and succeeds, its cross_endpoint_modifier is applied to all other results' severity scores (clamped to 1.0). Maximum 10 endpoints per request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","maxLength":10000,"description":"Text content to analyze"},"content":{"type":"string","maxLength":10000,"description":"Alias for text (backward compatibility)"},"endpoints":{"type":"array","items":{"type":"string","enum":["social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","bullying","grooming","unsafe","synthetic-content"]},"minItems":1,"maxItems":10,"description":"List of endpoint names to run analysis against"},"detections":{"type":"array","items":{"type":"string","enum":["social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","bullying","grooming","unsafe","synthetic-content"]},"minItems":1,"maxItems":10,"description":"Alias for endpoints (backward compatibility)"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"age_group":{"type":"string"},"ageGroup":{"type":"string","description":"Alias for age_group (camelCase accepted)"},"platform":{"type":"string"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code"},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting. When omitted here, the account-level `default_conversation_type` is used if one is set."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Minimum risk level required to include support/helpline resources. Defaults to \"high\". Critical severity always included regardless of threshold."},"flag_profanity":{"type":"boolean","description":"Additive, deterministic word-list flag for plain profanity/vulgarity, applied to the bullying and unsafe results only. Adds `profanity` to those results; never affects detected/severity/risk_score/recommended_action. Free — no extra credits."},"flag_risk_terms":{"type":"boolean","description":"Additive, deterministic word-list flag for bare drug and violence terms, applied to the bullying and unsafe results only. Adds `risk_terms` to those results; never affects detected/severity/risk_score/recommended_action. Purely lexical: WILL fire on benign uses of an included term (\"kill the lights\"). Free — no extra credits."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Alias for options.support_threshold (top-level convenience field)."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"endpoint":{"type":"string"},"detected":{"type":"boolean"},"severity":{"type":"number"},"confidence":{"type":"number"},"risk_score":{"type":"number"},"level":{"type":"string"},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"evidence":{"type":"array","items":{"type":"object","additionalProperties":true}},"recommended_action":{"type":"string"},"rationale":{"type":"string"},"third_party_concern":{"type":"boolean","description":"Present on the unsafe result when the verdict rests on a child reporting, disclosing or reacting to a real risk rather than on anyone enacting one. Set only when the verdict rests on that report: if the analysis independently called the content unsafe, this is absent. It is a provenance hint, not a cap -- always route on recommended_action, which can be anything up to immediate_intervention."},"third_party_concern_reason":{"type":"string","description":"Human-readable explanation. Present only when third_party_concern is true."},"speaker_role":{"type":"string","description":"Present on the bullying result (and on detectors that report it natively). Who the speaker is relative to the conduct: `enacting`, `experiencing`, `reporting_other`, `self_directed` or `unclear`. The only field that tells the person being bullied from the person doing it, since `detected` reports presence for both. Same value the dedicated /safety/bullying route returns."},"speaker_stance":{"type":"string","description":"Present on the grooming result (and on detectors that report it natively) when the model gave one: `deploying`, `describing_third_party` or `unclear`."},"verdict_capped":{"type":"boolean","description":"Grooming result: the verdict was capped. `verdict_capped_reason` says why. Absent when no cap applied."},"verdict_capped_reason":{"type":"string"},"verdict_withdrawn":{"type":"boolean","description":"Grooming result: the verdict was withdrawn as unsupported. `verdict_withdrawn_reason` says why. Absent otherwise."},"verdict_withdrawn_reason":{"type":"string"},"disclosure_cleared":{"type":"boolean","description":"Grooming result: the content is a disclosure or report of grooming, not an incident of it, and the verdict was cleared on that basis. `disclosure_cleared_reason` says why. Absent otherwise."},"disclosure_cleared_reason":{"type":"string"},"secrecy_frame_cleared":{"type":"boolean","description":"Grooming result: a secrecy tactic was cleared because the confidentiality described includes the guardian or is the limited kind a professional states. Absent otherwise."},"confidentiality_frame":{"type":"string","description":"Grooming result: how the conversation frames secrecy, as the dedicated /safety/grooming route returns it: `unconditional_concealment`, `limited_confidentiality`, `guardian_included` or `none`."},"action_capped":{"type":"boolean","description":"The recommended action was lowered. `action_capped_reason` says why. Absent when no cap applied."},"action_capped_reason":{"type":"string"},"escalation_capped":{"type":"boolean","description":"Bullying and unsafe results: an escalation was capped. `escalation_capped_reason` says why. Absent when no cap applied."},"escalation_capped_reason":{"type":"string"},"number_coincidence_cleared":{"type":"boolean","description":"Radicalisation, unsafe, bullying and grooming results: an alert rested on a number in ordinary use (an address, a count, a price) and was cleared. `number_coincidence_cleared_reason` says why. Absent otherwise."},"number_coincidence_cleared_reason":{"type":"string"},"protective_speech_capped":{"type":"boolean","description":"An alert on a text that works against the harm it describes was lowered, to `monitor` when nobody is at risk in it and otherwise to `flag_for_review` and no lower. `protective_speech_capped_reason` says which and why. Absent unless it applied."},"protective_speech_capped_reason":{"type":"string"},"urgency_kept":{"type":"boolean","description":"An urgent action a declared setting would have capped to review was kept, because the case is acute."},"urgency_kept_reason":{"type":"string"},"game_play_capped":{"type":"boolean","description":"Radicalisation result: an alert was lowered to `monitor` because everything in the text happens inside a game, a story or a sport, with nothing aimed at a real person, group or place. `game_play_capped_reason` says why. Absent otherwise."},"game_play_capped_reason":{"type":"string"},"passing_remark_capped":{"type":"boolean","description":"Bullying result: a review alert was lowered to `monitor` because the remark was about play or the moment and not about the person, or because it was a surprise for the person or a rough word said with plain affection, with nothing going at the person. `passing_remark_capped_reason` says why. Absent unless it applied."},"passing_remark_capped_reason":{"type":"string"},"single_insult_watched":{"type":"boolean","description":"Bullying result: the analysis found bullying by the speaker and asked for no action, and the action was set to `monitor` so that it can be counted. Absent otherwise."},"single_insult_watched_reason":{"type":"string"},"bullying_report_surfaced":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, a separate reading found the speaker telling of a specific person being mistreated by peers and still going on, and the result was sent for review. Absent otherwise."},"bullying_report_surfaced_reason":{"type":"string"},"rumour_spread_raised":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, a separate reading found the writer passing on something damaging about a specific absent person to others, and the result was sent for review. Absent otherwise."},"rumour_spread_raised_reason":{"type":"string"},"looks_attack_raised":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, a separate reading found a put-down about a person's body or looks, and the result was sent for review. Absent otherwise."},"looks_attack_raised_reason":{"type":"string"},"continued_after_objection_raised":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, and a separate reading of the conversation found that one person objected and the other carried on. Sent for review."},"continued_after_objection_raised_reason":{"type":"string"},"declared_age_overridden":{"type":"boolean","description":"A declared adult age was dropped because the speaker states a minor age. `declared_age_overridden_reason` says why. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string"},"willing_exchange_capped":{"type":"boolean","description":"Gender-based violence result: a review alert was lowered to `monitor` because the message was about something two people share willingly, with no minor declared or indicated. `willing_exchange_capped_reason` says why. Absent unless it applied."},"willing_exchange_capped_reason":{"type":"string"},"document_safekeeping_capped":{"type":"boolean","description":"Coercive-control result: a review alert was lowered to `monitor` because the message was about documents held for safekeeping, with nothing saying the owner wants them and cannot have them. `document_safekeeping_capped_reason` says why. Absent unless it applied."},"document_safekeeping_capped_reason":{"type":"string"},"ordinary_discipline_capped":{"type":"boolean","description":"Coercive-control result: a review alert was lowered to `monitor` because the text was a parent or carer giving a child a rule or a consequence with a stated reason and an end, with no basic need or contact taken away and nobody frightened, degraded or threatened. `ordinary_discipline_capped_reason` says why. Absent unless it applied."},"ordinary_discipline_capped_reason":{"type":"string"},"ordinary_day_capped":{"type":"boolean","description":"Distress-signals result: a review alert naming only family conflict, withdrawal or trust seeking was lowered to `monitor` because the message mentions an ordinary part of life and nothing more. `ordinary_day_capped_reason` says why. Absent unless it applied."},"ordinary_day_capped_reason":{"type":"string"},"support_suggested":{"type":"boolean","description":"Distress-signals result: the analysis suggested a supportive check-in and nothing more. `recommended_action` is `monitor`. Absent otherwise."},"third_party_report_surfaced":{"type":"boolean","description":"Vulnerability-exploitation result: the speaker is reporting that a specific person, not in the conversation, is being exploited now, with nobody protective involved yet. Kept and sent for review as a report, never as an action against the reporter. `third_party_report_surfaced_reason` says why. Absent unless it applied."},"third_party_report_surfaced_reason":{"type":"string"},"adult_exchange_capped":{"type":"boolean","description":"Grooming result: an alert was lowered to `low` / `monitor` because no child was declared or stated anywhere in the conversation and a separate reading found that the people taking part are stated to be adults, with no sign of a minor and no pressure. `adult_exchange_capped_reason` says why. Absent unless it applied."},"adult_exchange_capped_reason":{"type":"string"},"shared_surprise_capped":{"type":"boolean","description":"Grooming result: a review alert resting on secrecy and its cover story was lowered to `low` / `monitor` because the only thing kept quiet is a surprise for the person it is kept from, with a parent or carer of the child part of the plan. `shared_surprise_capped_reason` says why. Absent unless it applied."},"shared_surprise_capped_reason":{"type":"string"},"meeting_logistics_cleared":{"type":"boolean","description":"Grooming result: a single message flagged only for an arrangement to meet was cleared because a separate reading found practical detail for an ordinary purpose and nothing more. Absent otherwise."},"meeting_logistics_cleared_reason":{"type":"string"},"long_conversation_raised":{"type":"boolean","description":"Grooming result: a long conversation was read a second time with a few messages singled out for close reading, and the result shown is that second reading. Absent otherwise."},"long_conversation_raised_reason":{"type":"string"},"figurative_low_mood_capped":{"type":"boolean","description":"Unsafe result: a review alert whose only concern was low mood was lowered to `monitor` because the words were not meant literally. `figurative_low_mood_capped_reason` says why. Absent unless it applied."},"figurative_low_mood_capped_reason":{"type":"string"},"banter_capped":{"type":"boolean","description":"Unsafe result: an alert that a rule for declared minors had raised was lowered one step (review to `monitor`, or the most urgent action to `flag_for_review`) because two separate readings found banter between people who know each other, and the analysis named no acute marker. `banter_capped_reason` says which step and why. Absent unless it applied."},"banter_capped_reason":{"type":"string"},"no_child_shown_capped":{"type":"boolean","description":"Unsafe result: no age group was declared, and a review alert on something ordinary adult life is mistaken for was lowered to `monitor` because nothing in the message shows a child and nobody in it is harmed. `no_child_shown_capped_reason` says why. Absent unless it applied."},"no_child_shown_capped_reason":{"type":"string"},"conversation_distress_surfaced":{"type":"boolean","description":"Unsafe result: the latest message showed nothing, and a separate reading of the whole conversation found distress or an acute statement in an earlier turn. `conversation_distress_surfaced_reason` says which. Absent otherwise."},"conversation_distress_surfaced_reason":{"type":"string"},"logistics_only_capped":{"type":"boolean","description":"Unsafe result: an alert that rested on a location or an arrangement alone was cleared. `logistics_only_capped_reason` says why. Absent otherwise."},"logistics_only_capped_reason":{"type":"string"},"credits_used":{"type":"number"},"profanity":{"type":["object","null"],"description":"Present only on the bullying/unsafe results when options.flag_profanity (or the account-level default) is true. Deterministic word-list result — additive, never affects detected/severity/risk_score/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"string"}}}},"risk_terms":{"type":["object","null"],"description":"Present only on the bullying/unsafe results when options.flag_risk_terms (or the account-level default) is true. Deterministic drug/violence word-list result — additive, never affects detected/severity/risk_score/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"category":{"type":"string","enum":["drug","violence"]}}}}}}}}},"summary":{"type":"object","properties":{"total_endpoints":{"type":"number"},"detected_count":{"type":"number"},"highest_risk":{"type":"object","description":"The primary classification. `endpoint` is the most SPECIFIC detected pattern within 0.10 of the top score, not necessarily the highest-scoring one: a broad label such as `bullying` loses to a named relational or fraud pattern that scores slightly lower. `risk_score` is that endpoint's own score. Note it is therefore NOT the value `overall_risk_level` is derived from -- that always uses the maximum score across every endpoint, so the choice of label can never change the severity band.","properties":{"endpoint":{"type":"string"},"risk_score":{"type":"number"},"close_calls":{"type":"array","description":"Other detected endpoints within 0.05 of the top risk_score, present only when there is one -- do not treat \"endpoint\" above as the sole classification when this is non-empty. These detectors genuinely agree on multi-harm content, so several endpoints clustering at a similar score is the NORMAL result and not a ranking failure. The scores are deliberately not spread apart to force an ordering: run-to-run variance on this pipeline puts roughly 0.02 of noise on a score, so a manufactured gap smaller than that would be invented precision. Read this array as the finding when it is non-empty, rather than sorting on `risk_score` alone.","items":{"type":"object","properties":{"endpoint":{"type":"string"},"risk_score":{"type":"number"}}}}}},"overall_risk_level":{"type":"string"},"processing_time_ms":{"type":"number"},"total_credits_used":{"type":"number"},"vulnerability_modifier_applied":{"type":"boolean"},"analysis_status":{"type":"string","enum":["complete","partial"],"description":"`complete` when every requested detector produced an analysis. `partial` when at least one could not, after one retry: `failed_endpoints` names them, and `recommended_action` asks for human review, because a detector that did not answer is not a detector that found nothing. If no detector answers at all the request fails with an error and there is no 200 response."},"recommended_action":{"type":"string","description":"Present only when `analysis_status` is `partial`: at least `flag_for_review`, and never less than the strongest action among the detectors that did answer. Each result in `results` carries its own `recommended_action` as usual."},"action_detail":{"type":"string","description":"Present only when `analysis_status` is `partial`. What could not be analysed, and what to do about it."},"failed_endpoints":{"type":"array","items":{"type":"object","properties":{"endpoint":{"type":"string","description":"The endpoint that failed"},"code":{"type":"string","description":"The error code, as on an error response"},"error":{"type":"string","description":"A public description of the failure"}}},"description":"Detectors that could not produce an analysis after one retry (only present if any failed)"},"skipped_endpoints":{"type":"array","items":{"type":"string"},"description":"Endpoints skipped because they are disabled in account detection settings"},"skipped_reason":{"type":"string","description":"Reason why endpoints were skipped"}}},"cross_endpoint_modifier":{"type":"number"},"support":{"type":"object","description":"Helpline and support resources relevant to the detected risk categories and country.","additionalProperties":true},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"}}}}}}}}},"/api/v1/detection/multi":{"post":{"summary":"Fan-out analysis across multiple detection endpoints","tags":["Analyse"],"description":"Submit text to multiple detection endpoints in a single request. All endpoints run in parallel via Promise.allSettled. If vulnerability-exploitation is included and succeeds, its cross_endpoint_modifier is applied to all other results' severity scores (clamped to 1.0). Maximum 10 endpoints per request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","maxLength":10000,"description":"Text content to analyze"},"content":{"type":"string","maxLength":10000,"description":"Alias for text (backward compatibility)"},"endpoints":{"type":"array","items":{"type":"string","enum":["social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","bullying","grooming","unsafe","synthetic-content"]},"minItems":1,"maxItems":10,"description":"List of endpoint names to run analysis against"},"detections":{"type":"array","items":{"type":"string","enum":["social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","bullying","grooming","unsafe","synthetic-content"]},"minItems":1,"maxItems":10,"description":"Alias for endpoints (backward compatibility)"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"age_group":{"type":"string"},"ageGroup":{"type":"string","description":"Alias for age_group (camelCase accepted)"},"platform":{"type":"string"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code"},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting. When omitted here, the account-level `default_conversation_type` is used if one is set."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Minimum risk level required to include support/helpline resources. Defaults to \"high\". Critical severity always included regardless of threshold."},"flag_profanity":{"type":"boolean","description":"Additive, deterministic word-list flag for plain profanity/vulgarity, applied to the bullying and unsafe results only. Adds `profanity` to those results; never affects detected/severity/risk_score/recommended_action. Free — no extra credits."},"flag_risk_terms":{"type":"boolean","description":"Additive, deterministic word-list flag for bare drug and violence terms, applied to the bullying and unsafe results only. Adds `risk_terms` to those results; never affects detected/severity/risk_score/recommended_action. Purely lexical: WILL fire on benign uses of an included term (\"kill the lights\"). Free — no extra credits."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Alias for options.support_threshold (top-level convenience field)."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"endpoint":{"type":"string"},"detected":{"type":"boolean"},"severity":{"type":"number"},"confidence":{"type":"number"},"risk_score":{"type":"number"},"level":{"type":"string"},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"evidence":{"type":"array","items":{"type":"object","additionalProperties":true}},"recommended_action":{"type":"string"},"rationale":{"type":"string"},"third_party_concern":{"type":"boolean","description":"Present on the unsafe result when the verdict rests on a child reporting, disclosing or reacting to a real risk rather than on anyone enacting one. Set only when the verdict rests on that report: if the analysis independently called the content unsafe, this is absent. It is a provenance hint, not a cap -- always route on recommended_action, which can be anything up to immediate_intervention."},"third_party_concern_reason":{"type":"string","description":"Human-readable explanation. Present only when third_party_concern is true."},"speaker_role":{"type":"string","description":"Present on the bullying result (and on detectors that report it natively). Who the speaker is relative to the conduct: `enacting`, `experiencing`, `reporting_other`, `self_directed` or `unclear`. The only field that tells the person being bullied from the person doing it, since `detected` reports presence for both. Same value the dedicated /safety/bullying route returns."},"speaker_stance":{"type":"string","description":"Present on the grooming result (and on detectors that report it natively) when the model gave one: `deploying`, `describing_third_party` or `unclear`."},"verdict_capped":{"type":"boolean","description":"Grooming result: the verdict was capped. `verdict_capped_reason` says why. Absent when no cap applied."},"verdict_capped_reason":{"type":"string"},"verdict_withdrawn":{"type":"boolean","description":"Grooming result: the verdict was withdrawn as unsupported. `verdict_withdrawn_reason` says why. Absent otherwise."},"verdict_withdrawn_reason":{"type":"string"},"disclosure_cleared":{"type":"boolean","description":"Grooming result: the content is a disclosure or report of grooming, not an incident of it, and the verdict was cleared on that basis. `disclosure_cleared_reason` says why. Absent otherwise."},"disclosure_cleared_reason":{"type":"string"},"secrecy_frame_cleared":{"type":"boolean","description":"Grooming result: a secrecy tactic was cleared because the confidentiality described includes the guardian or is the limited kind a professional states. Absent otherwise."},"confidentiality_frame":{"type":"string","description":"Grooming result: how the conversation frames secrecy, as the dedicated /safety/grooming route returns it: `unconditional_concealment`, `limited_confidentiality`, `guardian_included` or `none`."},"action_capped":{"type":"boolean","description":"The recommended action was lowered. `action_capped_reason` says why. Absent when no cap applied."},"action_capped_reason":{"type":"string"},"escalation_capped":{"type":"boolean","description":"Bullying and unsafe results: an escalation was capped. `escalation_capped_reason` says why. Absent when no cap applied."},"escalation_capped_reason":{"type":"string"},"number_coincidence_cleared":{"type":"boolean","description":"Radicalisation, unsafe, bullying and grooming results: an alert rested on a number in ordinary use (an address, a count, a price) and was cleared. `number_coincidence_cleared_reason` says why. Absent otherwise."},"number_coincidence_cleared_reason":{"type":"string"},"protective_speech_capped":{"type":"boolean","description":"An alert on a text that works against the harm it describes was lowered, to `monitor` when nobody is at risk in it and otherwise to `flag_for_review` and no lower. `protective_speech_capped_reason` says which and why. Absent unless it applied."},"protective_speech_capped_reason":{"type":"string"},"urgency_kept":{"type":"boolean","description":"An urgent action a declared setting would have capped to review was kept, because the case is acute."},"urgency_kept_reason":{"type":"string"},"game_play_capped":{"type":"boolean","description":"Radicalisation result: an alert was lowered to `monitor` because everything in the text happens inside a game, a story or a sport, with nothing aimed at a real person, group or place. `game_play_capped_reason` says why. Absent otherwise."},"game_play_capped_reason":{"type":"string"},"passing_remark_capped":{"type":"boolean","description":"Bullying result: a review alert was lowered to `monitor` because the remark was about play or the moment and not about the person, or because it was a surprise for the person or a rough word said with plain affection, with nothing going at the person. `passing_remark_capped_reason` says why. Absent unless it applied."},"passing_remark_capped_reason":{"type":"string"},"single_insult_watched":{"type":"boolean","description":"Bullying result: the analysis found bullying by the speaker and asked for no action, and the action was set to `monitor` so that it can be counted. Absent otherwise."},"single_insult_watched_reason":{"type":"string"},"bullying_report_surfaced":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, a separate reading found the speaker telling of a specific person being mistreated by peers and still going on, and the result was sent for review. Absent otherwise."},"bullying_report_surfaced_reason":{"type":"string"},"rumour_spread_raised":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, a separate reading found the writer passing on something damaging about a specific absent person to others, and the result was sent for review. Absent otherwise."},"rumour_spread_raised_reason":{"type":"string"},"looks_attack_raised":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, a separate reading found a put-down about a person's body or looks, and the result was sent for review. Absent otherwise."},"looks_attack_raised_reason":{"type":"string"},"continued_after_objection_raised":{"type":"boolean","description":"Bullying result: the analysis asked for nothing above `monitor`, and a separate reading of the conversation found that one person objected and the other carried on. Sent for review."},"continued_after_objection_raised_reason":{"type":"string"},"declared_age_overridden":{"type":"boolean","description":"A declared adult age was dropped because the speaker states a minor age. `declared_age_overridden_reason` says why. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string"},"willing_exchange_capped":{"type":"boolean","description":"Gender-based violence result: a review alert was lowered to `monitor` because the message was about something two people share willingly, with no minor declared or indicated. `willing_exchange_capped_reason` says why. Absent unless it applied."},"willing_exchange_capped_reason":{"type":"string"},"document_safekeeping_capped":{"type":"boolean","description":"Coercive-control result: a review alert was lowered to `monitor` because the message was about documents held for safekeeping, with nothing saying the owner wants them and cannot have them. `document_safekeeping_capped_reason` says why. Absent unless it applied."},"document_safekeeping_capped_reason":{"type":"string"},"ordinary_discipline_capped":{"type":"boolean","description":"Coercive-control result: a review alert was lowered to `monitor` because the text was a parent or carer giving a child a rule or a consequence with a stated reason and an end, with no basic need or contact taken away and nobody frightened, degraded or threatened. `ordinary_discipline_capped_reason` says why. Absent unless it applied."},"ordinary_discipline_capped_reason":{"type":"string"},"ordinary_day_capped":{"type":"boolean","description":"Distress-signals result: a review alert naming only family conflict, withdrawal or trust seeking was lowered to `monitor` because the message mentions an ordinary part of life and nothing more. `ordinary_day_capped_reason` says why. Absent unless it applied."},"ordinary_day_capped_reason":{"type":"string"},"support_suggested":{"type":"boolean","description":"Distress-signals result: the analysis suggested a supportive check-in and nothing more. `recommended_action` is `monitor`. Absent otherwise."},"third_party_report_surfaced":{"type":"boolean","description":"Vulnerability-exploitation result: the speaker is reporting that a specific person, not in the conversation, is being exploited now, with nobody protective involved yet. Kept and sent for review as a report, never as an action against the reporter. `third_party_report_surfaced_reason` says why. Absent unless it applied."},"third_party_report_surfaced_reason":{"type":"string"},"adult_exchange_capped":{"type":"boolean","description":"Grooming result: an alert was lowered to `low` / `monitor` because no child was declared or stated anywhere in the conversation and a separate reading found that the people taking part are stated to be adults, with no sign of a minor and no pressure. `adult_exchange_capped_reason` says why. Absent unless it applied."},"adult_exchange_capped_reason":{"type":"string"},"shared_surprise_capped":{"type":"boolean","description":"Grooming result: a review alert resting on secrecy and its cover story was lowered to `low` / `monitor` because the only thing kept quiet is a surprise for the person it is kept from, with a parent or carer of the child part of the plan. `shared_surprise_capped_reason` says why. Absent unless it applied."},"shared_surprise_capped_reason":{"type":"string"},"meeting_logistics_cleared":{"type":"boolean","description":"Grooming result: a single message flagged only for an arrangement to meet was cleared because a separate reading found practical detail for an ordinary purpose and nothing more. Absent otherwise."},"meeting_logistics_cleared_reason":{"type":"string"},"long_conversation_raised":{"type":"boolean","description":"Grooming result: a long conversation was read a second time with a few messages singled out for close reading, and the result shown is that second reading. Absent otherwise."},"long_conversation_raised_reason":{"type":"string"},"figurative_low_mood_capped":{"type":"boolean","description":"Unsafe result: a review alert whose only concern was low mood was lowered to `monitor` because the words were not meant literally. `figurative_low_mood_capped_reason` says why. Absent unless it applied."},"figurative_low_mood_capped_reason":{"type":"string"},"banter_capped":{"type":"boolean","description":"Unsafe result: an alert that a rule for declared minors had raised was lowered one step (review to `monitor`, or the most urgent action to `flag_for_review`) because two separate readings found banter between people who know each other, and the analysis named no acute marker. `banter_capped_reason` says which step and why. Absent unless it applied."},"banter_capped_reason":{"type":"string"},"no_child_shown_capped":{"type":"boolean","description":"Unsafe result: no age group was declared, and a review alert on something ordinary adult life is mistaken for was lowered to `monitor` because nothing in the message shows a child and nobody in it is harmed. `no_child_shown_capped_reason` says why. Absent unless it applied."},"no_child_shown_capped_reason":{"type":"string"},"conversation_distress_surfaced":{"type":"boolean","description":"Unsafe result: the latest message showed nothing, and a separate reading of the whole conversation found distress or an acute statement in an earlier turn. `conversation_distress_surfaced_reason` says which. Absent otherwise."},"conversation_distress_surfaced_reason":{"type":"string"},"logistics_only_capped":{"type":"boolean","description":"Unsafe result: an alert that rested on a location or an arrangement alone was cleared. `logistics_only_capped_reason` says why. Absent otherwise."},"logistics_only_capped_reason":{"type":"string"},"credits_used":{"type":"number"},"profanity":{"type":["object","null"],"description":"Present only on the bullying/unsafe results when options.flag_profanity (or the account-level default) is true. Deterministic word-list result — additive, never affects detected/severity/risk_score/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"string"}}}},"risk_terms":{"type":["object","null"],"description":"Present only on the bullying/unsafe results when options.flag_risk_terms (or the account-level default) is true. Deterministic drug/violence word-list result — additive, never affects detected/severity/risk_score/recommended_action.","properties":{"detected":{"type":"boolean"},"matches":{"type":"array","items":{"type":"object","properties":{"term":{"type":"string"},"category":{"type":"string","enum":["drug","violence"]}}}}}}}}},"summary":{"type":"object","properties":{"total_endpoints":{"type":"number"},"detected_count":{"type":"number"},"highest_risk":{"type":"object","description":"The primary classification. `endpoint` is the most SPECIFIC detected pattern within 0.10 of the top score, not necessarily the highest-scoring one: a broad label such as `bullying` loses to a named relational or fraud pattern that scores slightly lower. `risk_score` is that endpoint's own score. Note it is therefore NOT the value `overall_risk_level` is derived from -- that always uses the maximum score across every endpoint, so the choice of label can never change the severity band.","properties":{"endpoint":{"type":"string"},"risk_score":{"type":"number"},"close_calls":{"type":"array","description":"Other detected endpoints within 0.05 of the top risk_score, present only when there is one -- do not treat \"endpoint\" above as the sole classification when this is non-empty. These detectors genuinely agree on multi-harm content, so several endpoints clustering at a similar score is the NORMAL result and not a ranking failure. The scores are deliberately not spread apart to force an ordering: run-to-run variance on this pipeline puts roughly 0.02 of noise on a score, so a manufactured gap smaller than that would be invented precision. Read this array as the finding when it is non-empty, rather than sorting on `risk_score` alone.","items":{"type":"object","properties":{"endpoint":{"type":"string"},"risk_score":{"type":"number"}}}}}},"overall_risk_level":{"type":"string"},"processing_time_ms":{"type":"number"},"total_credits_used":{"type":"number"},"vulnerability_modifier_applied":{"type":"boolean"},"analysis_status":{"type":"string","enum":["complete","partial"],"description":"`complete` when every requested detector produced an analysis. `partial` when at least one could not, after one retry: `failed_endpoints` names them, and `recommended_action` asks for human review, because a detector that did not answer is not a detector that found nothing. If no detector answers at all the request fails with an error and there is no 200 response."},"recommended_action":{"type":"string","description":"Present only when `analysis_status` is `partial`: at least `flag_for_review`, and never less than the strongest action among the detectors that did answer. Each result in `results` carries its own `recommended_action` as usual."},"action_detail":{"type":"string","description":"Present only when `analysis_status` is `partial`. What could not be analysed, and what to do about it."},"failed_endpoints":{"type":"array","items":{"type":"object","properties":{"endpoint":{"type":"string","description":"The endpoint that failed"},"code":{"type":"string","description":"The error code, as on an error response"},"error":{"type":"string","description":"A public description of the failure"}}},"description":"Detectors that could not produce an analysis after one retry (only present if any failed)"},"skipped_endpoints":{"type":"array","items":{"type":"string"},"description":"Endpoints skipped because they are disabled in account detection settings"},"skipped_reason":{"type":"string","description":"Reason why endpoints were skipped"}}},"cross_endpoint_modifier":{"type":"number"},"support":{"type":"object","description":"Helpline and support resources relevant to the detected risk categories and country.","additionalProperties":true},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"}}}}}}}}},"/api/v1/verify/challenge":{"get":{"summary":"Get a signed liveness challenge nonce","tags":["Verification"],"description":"Returns a server-signed nonce that must be included in the liveness token. This prevents client-side forgery of liveness tokens.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"nonce":{"type":"string"},"signature":{"type":"string"},"expires_at":{"type":"number"}}}}}}}}},"/api/v1/verify/submit":{"post":{"summary":"Submit documents for age verification","tags":["Verification"],"description":"Upload ID front, ID back, optional passport, and a selfie with liveness token for age verification. Returns verification result including age, face match, and liveness status.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_id":{"type":["string","null"]},"status":{"type":"string","enum":["verified","failed","needs_review"]},"age":{"type":["number","null"]},"date_of_birth":{"type":["string","null"]},"is_minor":{"type":["boolean","null"]},"document":{"type":"object","properties":{"ocr_confidence":{"type":"number"},"name_extracted":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"dob_extracted":{"type":["string","null"]},"document_number":{"type":["string","null"]},"document_number_valid":{"type":["boolean","null"]},"sex":{"type":["string","null"]},"country_code":{"type":["string","null"]},"country_name":{"type":["string","null"]},"document_type":{"type":["string","null"]},"expiration_date":{"type":["string","null"]},"expired":{"type":["boolean","null"]},"issue_date":{"type":["string","null"]},"height_cm":{"type":["number","null"]},"place_of_birth":{"type":["string","null"]},"nationality":{"type":["string","null"]},"issuing_authority":{"type":["string","null"]},"national_id":{"type":["object","null"],"properties":{"value":{"type":"string"},"valid":{"type":"boolean"},"type":{"type":"string"},"country_code":{"type":"string"}}},"mrz_valid":{"type":["boolean","null"]},"mrz_fields":{"type":["object","null"],"properties":{"document_number":{"type":["string","null"]},"nationality":{"type":["string","null"]},"date_of_birth":{"type":["string","null"]},"expiry_date":{"type":["string","null"]},"sex":{"type":["string","null"]},"surname":{"type":["string","null"]},"given_names":{"type":["string","null"]}}}}},"face_match":{"type":["object","null"],"properties":{"matched":{"type":"boolean"},"distance":{"type":"number"},"confidence":{"type":"number"}}},"liveness":{"type":"object","properties":{"valid":{"type":"boolean"},"reason":{"type":"string"}}},"credits_used":{"type":"number"}}}}}}}}},"/api/v1/verify/{verificationId}":{"get":{"summary":"Retrieve a past verification result","tags":["Verification"],"description":"Get the result of a previous age verification by its ID. Only the owning API key can retrieve its own verifications.","parameters":[{"schema":{"type":"string"},"in":"path","name":"verificationId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_id":{"type":"string"},"status":{"type":"string"},"age":{"type":["number","null"]},"is_minor":{"type":["boolean","null"]},"ocr_confidence":{"type":"number"},"face_matched":{"type":["boolean","null"]},"face_confidence":{"type":["number","null"]},"liveness_valid":{"type":"boolean"},"failure_reasons":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string"}}}}}}}}},"/api/v1/verify-image-match":{"post":{"summary":"Composite non-LLM image verification","tags":["Verification"],"description":"Run a configurable set of forensic checks over a single image in one call: face matching against a reference image, age estimation, NSFW/skin-exposure forensics, and synthetic-image detection (deep-image classifier + C2PA provenance). All checks are deterministic/model-based — no LLM ever sees the image. Images are processed in memory only and never stored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["image"],"properties":{"image":{"type":"string","maxLength":20971524,"description":"The image to verify, base64-encoded (max 15MB decoded). Processed transiently — never stored."},"reference_image":{"type":"string","maxLength":20971524,"description":"Optional reference image (base64) for the face_match check. Without it, face_match is skipped even when requested."},"checks":{"type":"array","items":{"type":"string","enum":["face_match","age","forensic","synthetic"]},"maxItems":4,"description":"Subset of checks to run. Defaults to all of: face_match, age, forensic, synthetic."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"schema_version":{"type":"string","description":"Response schema version (currently \"1.0\")"},"checks_run":{"type":"array","items":{"type":"string"},"description":"The checks that actually ran (face_match is excluded when no reference_image was provided)"},"face_match":{"type":"object","properties":{"matched":{"type":"boolean","description":"Whether the two faces match"},"similarity":{"type":"number","description":"Face similarity (0.0-1.0)"},"reason":{"type":"string","description":"Why matching failed (e.g. \"no_face_detected\")"}}},"age":{"type":["object","null"],"properties":{"estimated_age":{"type":"number","description":"Estimated age of the detected face in years"},"confidence":{"type":"number","description":"Face-detection confidence (0.0-1.0)"}},"description":"Age estimate for the primary image, or null when no face was detected"},"forensic":{"type":"object","properties":{"nsfw":{"type":"boolean","description":"Whether the image was classified as explicit"},"nsfw_confidence":{"type":"number","description":"NSFW classifier score (0.0-1.0)"},"skin_exposure_level":{"type":"string","enum":["low","moderate","high"],"description":"Coarse skin-exposure level"}}},"synthetic":{"type":"object","properties":{"is_synthetic":{"type":"boolean","description":"Deep-image classifier verdict (absent when the service is unavailable)"},"synthetic_probability":{"type":"number","description":"Deep-image synthetic probability (0.0-1.0)"},"has_c2pa":{"type":"boolean","description":"A C2PA / Content Credentials manifest is embedded"},"is_ai_generated":{"type":"boolean","description":"The C2PA manifest declares AI generation"}}},"credits_used":{"type":"number","description":"Credits charged for this call"},"processing_ms":{"type":"number","description":"Total processing time in milliseconds"},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"}}}}}}}}},"/api/v1/verify/age/config":{"get":{"summary":"Get age verification mode configuration","tags":["Age Verification"],"description":"Returns the verification mode (full or liveness_only) and age threshold for the authenticated user. Pass `country` (ISO 3166-1 alpha-2) and optionally `region` to get market-specific overrides.","parameters":[{"schema":{"type":"string"},"in":"query","name":"country","required":false,"description":"ISO 3166-1 alpha-2 country code (e.g. DE, SE, US)"},{"schema":{"type":"string"},"in":"query","name":"region","required":false,"description":"Region/state code (e.g. CA, LA for US states)"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["full","liveness_only"]},"age_threshold":{"type":"number"},"country":{"type":"string"},"market":{"type":"string"}}}}}}}}},"/api/v1/verify/age/validate-document":{"post":{"summary":"Validate a document image (per-step validation)","tags":["Age Verification"],"description":"Checks image quality (resolution, luminance, contrast) without running OCR. Free — no credits consumed.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"quality":{"type":"string","enum":["good","poor","invalid"]},"issues":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/verify/age/validate-selfie":{"post":{"summary":"Validate a selfie image (per-step validation)","tags":["Age Verification"],"description":"Checks if a face is detected in the image. Free — no credits consumed.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"face_detected":{"type":"boolean"},"message":{"type":["string","null"]}}}}}}}}},"/api/v1/verify/age/submit":{"post":{"summary":"Submit documents for age verification","tags":["Age Verification"],"description":"Privacy-first age verification. Returns only age bracket (18+, 13-17, under_13), is_minor status, liveness, and face match. No PII (name, DOB, document number) is returned.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_id":{"type":["string","null"]},"status":{"type":"string","enum":["verified","failed","needs_review"]},"age_bracket":{"type":["string","null"]},"is_minor":{"type":["boolean","null"]},"liveness":{"type":"object","properties":{"valid":{"type":"boolean"},"reason":{"type":"string"}}},"face_match":{"type":["object","null"],"properties":{"matched":{"type":"boolean"},"confidence":{"type":"number"}}},"credits_used":{"type":"number"},"audit_receipt":{"type":["object","null"],"description":"EU AI Act Art 12 audit receipt — signed proof we processed and immediately destroyed the input. Verifiable out-of-band.","properties":{"request_id":{"type":"string","description":"Use this to fetch the full signed receipt via GET /audit/receipts/:request_id."},"timestamp":{"type":"string","description":"ISO 8601 timestamp when the receipt was signed."},"signature":{"type":"string","description":"Cryptographic signature (base64)."}}}}}}}}}}},"/api/v1/verify/age/{verificationId}":{"get":{"summary":"Retrieve a past age verification result","tags":["Age Verification"],"description":"Returns the age bracket, is_minor status, and verification metadata. No PII returned.","parameters":[{"schema":{"type":"string"},"in":"path","name":"verificationId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_id":{"type":"string"},"status":{"type":"string"},"is_minor":{"type":["boolean","null"]},"face_matched":{"type":["boolean","null"]},"liveness_valid":{"type":"boolean"},"failure_reasons":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string"}}}}}}}}},"/api/v1/verify/identity/validate-document":{"post":{"summary":"Validate a document image (per-step validation)","tags":["Identity Verification"],"description":"Checks image quality (resolution, luminance, contrast) without running OCR. Free — no credits consumed.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"quality":{"type":"string","enum":["good","poor","invalid"]},"issues":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/verify/identity/validate-selfie":{"post":{"summary":"Validate a selfie image (per-step validation)","tags":["Identity Verification"],"description":"Checks if a face is detected in the image. Free — no credits consumed.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean"},"face_detected":{"type":"boolean"},"message":{"type":["string","null"]}}}}}}}}},"/api/v1/verify/identity/submit":{"post":{"summary":"Submit documents for identity verification","tags":["Identity Verification"],"description":"Full KYC identity verification. Returns full name, date of birth, document type, country code, liveness, and face match. Available on Business and Enterprise plans only.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_id":{"type":["string","null"]},"status":{"type":"string","enum":["verified","failed","needs_review"]},"full_name":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"date_of_birth":{"type":["string","null"]},"document_type":{"type":["string","null"]},"country_code":{"type":["string","null"]},"country_name":{"type":["string","null"]},"document_number":{"type":["string","null"]},"document_number_valid":{"type":["boolean","null"]},"sex":{"type":["string","null"]},"nationality":{"type":["string","null"]},"place_of_birth":{"type":["string","null"]},"height_cm":{"type":["number","null"]},"issuing_authority":{"type":["string","null"]},"issue_date":{"type":["string","null"]},"expiration_date":{"type":["string","null"]},"national_id":{"type":["object","null"],"properties":{"value":{"type":"string"},"valid":{"type":"boolean"},"type":{"type":"string"},"country_code":{"type":"string"}}},"mrz_valid":{"type":["boolean","null"]},"liveness":{"type":"object","properties":{"valid":{"type":"boolean"},"reason":{"type":"string"}}},"face_match":{"type":["object","null"],"properties":{"matched":{"type":"boolean"},"distance":{"type":"number"},"confidence":{"type":"number"}}},"credits_used":{"type":"number"}}}}}}}}},"/api/v1/verify/identity/{verificationId}":{"get":{"summary":"Retrieve a past identity verification result","tags":["Identity Verification"],"description":"Returns full identity verification data. Business/Enterprise only.","parameters":[{"schema":{"type":"string"},"in":"path","name":"verificationId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"verification_id":{"type":"string"},"status":{"type":"string"},"face_matched":{"type":["boolean","null"]},"face_confidence":{"type":["number","null"]},"liveness_valid":{"type":"boolean"},"failure_reasons":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string"}}}}}}}}},"/api/v1/verify/session":{"post":{"summary":"Create a cross-device verification session","tags":["Verification"],"description":"Creates a session that can be used to complete verification on another device (e.g. mobile). Both `mode=age` and `mode=identity` require Assure or above. Returns a session ID and mobile URL with a one-time token.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["age","identity"],"default":"age"},"document_type":{"type":"string","enum":["drivers_license","national_id","passport"]}}}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string"},"mobile_url":{"type":"string"},"expires_at":{"type":"number"},"mode":{"type":"string"},"verification_mode":{"type":"string"},"recommended_image_width":{"type":"integer"}}}}}}}}},"/api/v1/verify/session/{id}":{"get":{"summary":"Get verification session status","tags":["Verification"],"description":"Returns the current status and outcome of a verification session. Only the API key that created the session can access it. The result carries the verification decision (status, age, is_minor, match/liveness/confidence signals) — never extracted document data (name, date of birth, document number), which is processed transiently and immediately destroyed. Integrations that need the full KYC fields use the synchronous POST /verify/identity flow, where the data is returned once and never stored.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"session_id":{"type":"string"},"status":{"type":"string"},"result":{"type":["object","null"],"additionalProperties":false,"properties":{"status":{"type":"string"},"age":{"type":["number","null"]},"is_minor":{"type":["boolean","null"]},"face_match":{"type":["object","null"],"additionalProperties":false,"properties":{"matched":{"type":"boolean"},"confidence":{"type":"number"}}},"liveness":{"type":"object","additionalProperties":false,"properties":{"valid":{"type":"boolean"}}},"date_of_birth":{"type":["string","null"]},"failure_reasons":{"type":["array","null"],"items":{"type":"string"}},"document":{"type":["object","null"],"additionalProperties":false,"properties":{"ocr_confidence":{"type":"number"},"name_extracted":{"type":["string","null"]},"first_name":{"type":["string","null"]},"last_name":{"type":["string","null"]},"dob_extracted":{"type":["string","null"]},"document_number":{"type":["string","null"]},"document_number_valid":{"type":["boolean","null"]},"sex":{"type":["string","null"]},"country_code":{"type":["string","null"]},"country_name":{"type":["string","null"]},"document_type":{"type":["string","null"]},"expiration_date":{"type":["string","null"]},"expired":{"type":["boolean","null"]},"issue_date":{"type":["string","null"]},"height_cm":{"type":["number","null"]},"place_of_birth":{"type":["string","null"]},"nationality":{"type":["string","null"]},"issuing_authority":{"type":["string","null"]},"national_id":{"type":["object","null"],"additionalProperties":false,"properties":{"value":{"type":"string"},"valid":{"type":"boolean"},"type":{"type":"string"},"country_code":{"type":"string"}}}}}}},"step_progress":{"type":["string","null"]},"created_at":{"type":"number"},"expires_at":{"type":"number"}}}}}}}},"delete":{"summary":"Cancel a verification session","tags":["Verification"],"description":"Cancels an active verification session. The mobile device will be notified via SSE.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}}}}},"/api/v1/safety/document":{"post":{"summary":"Analyze a PDF or image for safety and compliance concerns","tags":["Safety"],"description":"Upload a PDF (max 50MB, max 100 pages) or a single image (png, jpeg, webp) for multi-endpoint safety analysis. PDFs are read from the text layer per page; images are read by OCR and analysed as a one-page document. Choose which detectors run with the endpoints field. For a messaging screenshot, set split_by_sender=true to have the conversation split into messages with a side attributed to each, so the detector judges the last received message with the rest as history; this costs one additional vision call. Returns per-page results with an overall risk assessment, plus a SHA-256 hash of the uploaded file for chain-of-custody verification. Zero-retention: no document or image data is stored after processing.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string","description":"Client-provided file identifier (echoed back)"},"document_hash":{"type":"string","description":"SHA-256 hash of the uploaded PDF for chain-of-custody verification"},"total_pages":{"type":"number","description":"Total pages in the document"},"pages_analyzed":{"type":"number","description":"Pages with extractable text that were analyzed"},"extraction_summary":{"type":"object","properties":{"text_layer_pages":{"type":"number","description":"Pages where text was extracted from the PDF text layer (always 0 for image uploads)"},"ocr_pages":{"type":"number","description":"Pages read by OCR — every page of an image upload, and PDF pages with no usable text layer"},"failed_pages":{"type":"number","description":"Pages with insufficient text for analysis"},"average_ocr_confidence":{"type":"number","description":"Average OCR confidence (0.0-1.0) across OCR pages"}}},"page_results":{"type":"array","items":{"type":"object","properties":{"page_number":{"type":"number"},"text_preview":{"type":"string","description":"First 200 chars of page text"},"extraction_method":{"type":"string","enum":["text_layer","ocr"]},"ocr_confidence":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"endpoint":{"type":"string"},"detected":{"type":"boolean"},"severity":{"type":"number"},"confidence":{"type":"number"},"risk_score":{"type":"number"},"level":{"type":"string"},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"evidence":{"type":"array","items":{"type":"object","additionalProperties":true}},"recommended_action":{"type":"string"},"rationale":{"type":"string"}}}},"page_risk_score":{"type":"number"},"page_severity":{"type":"string"}}}},"overall_risk_score":{"type":"number","description":"Highest risk score across all pages"},"overall_severity":{"type":"string","enum":["none","low","medium","high","critical"]},"detected_endpoints":{"type":"array","items":{"type":"string"},"description":"Endpoints that detected threats"},"flagged_pages":{"type":"array","items":{"type":"object","properties":{"page_number":{"type":"number"},"risk_score":{"type":"number"},"severity":{"type":"string"},"detected_endpoints":{"type":"array","items":{"type":"string"}}}}},"credits_used":{"type":"number"},"processing_time_ms":{"type":"number"},"language":{"type":"string"},"language_status":{"type":"string"},"support":{"type":"object","additionalProperties":true},"external_id":{"type":"string"},"incident_moderation_enabled":{"type":"string","description":"Per-call override of account incident logging: \"true\" forces persistence, \"false\" suppresses it. Omit for account default."},"customer_id":{"type":"string"},"metadata":{"type":"object","additionalProperties":true}}}}}}}}},"/api/v1/safety/synthetic-content":{"post":{"summary":"Detect AI-generated or synthetic content","tags":["Safety"],"description":"Analyzes text for AI-generated content indicators including LLM-generated text, synthetic identities, AI-enhanced grooming scripts, and synthetic CSAM. Classifies content as confirmed_synthetic, suspected_synthetic, unknown, or confirmed_authentic.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","maxLength":10000,"description":"The text content to analyze"},"context":{"type":"object","properties":{"language":{"type":"string","description":"Language code (e.g., \"en\"). 32 supported languages: en is stable, the other 31 are beta (ar, bg, cs, da, de, el, es, et, fi, fr, ga, hr, hu, it, ja, ko, lt, lv, mt, nl, no, pl, pt, ro, ru, sk, sl, sv, tr, uk, zh). The code is the documented form; the English name of a supported language is also accepted (\"English\" as \"en\"). A region or script suffix is read as its base language (\"en-US\" as \"en\"), and \"nb\" or \"nn\" as \"no\". A value that is not supported is refused with a 400 that lists the supported codes. Omit the field, or send \"auto\", and the language is detected from the text."},"conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Optional, no default: omit it and behaviour is unchanged. The setting this conversation was captured in, declared by your platform from its own configuration, never from end-user input. A declared setting names the responsible party in the conversation: a counsellor or helpline worker (`helpline`, `clinical`), a clinician (`healthcare`), an officer or investigator (`law_enforcement`, `investigative`), an adviser taking a report (`fraud_report`, `abuse_report`, `support_disclosure`, `advisory`), a moderator (`moderation_review`), a lawyer, trainer, journalist or researcher (`legal`, `training`, `journalism`, `research`), a teacher, tutor or safeguarding lead (`education`), a parent or guardian (`family`). In any of these settings, a conversation in which a person is at risk, that responsible party is already handling it, and nothing is happening as the text is written, is kept visible at `monitor` without an alert, and `protective_speech_capped` says so. Harm being done in the conversation, by anyone including the responsible party, a person at risk with nobody acting, and anything acute alert as they would with no setting. Where the harm is discussed and not enacted (`fraud_report`, `abuse_report`, `support_disclosure`, `helpline`, `clinical`, `investigative`, `legal`, `advisory`, `training`, `journalism`, `research`, `moderation_review`) an urgent action is also capped to `flag_for_review`, and the response echoes `conversation_type_applied`. `direct_message`, `group_chat`, `public_post`, `customer_service` and `gaming_chat` change nothing. In `education` and `family` this applies only when the speaker is not the person at risk: a young person speaking for themselves to a teacher or a parent alerts as with no setting."},"age_group":{"type":"string","description":"Age group (e.g., \"13-15\")"},"ageGroup":{"type":"string","description":"Age group (camelCase alias, e.g., \"under 18\")"},"platform":{"type":"string","description":"Platform name (e.g., \"Discord\", \"Telegram\")"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 country code (e.g., \"GB\", \"US\", \"SE\") for geo-localised helpline data. Falls back to user profile country if omitted."},"prior_messages":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Prior turns of this conversation, oldest first, submitted by you for this request only -- Tuteliq stores no message content, so nothing here is retained after the call completes. Use this when you already have the whole conversation in hand; use continuation_token instead when you are calling once per new message as it arrives."},"conversation_history":{"type":"array","maxItems":100,"items":{"type":"object","required":["role","text"],"properties":{"role":{"type":"string","description":"Role of the sender (e.g., \"user\", \"contact\", \"adult\", \"child\")"},"text":{"type":"string","maxLength":10000,"description":"Message text"},"timestamp":{"type":"string","description":"ISO 8601 timestamp of the message (optional)"}}},"description":"Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility."}}},"options":{"type":"object","properties":{"include_evidence":{"type":"boolean","description":"Include evidence excerpts in response. Default: true."},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"default":"high","description":"Minimum severity level to include crisis support resources. Defaults to \"high\". Note: \"critical\" severity always includes support resources regardless of this setting."},"verdict_only":{"type":"boolean","default":false,"description":"Fast mode: omit the per-message `message_analysis` array from the response. All other fields are unchanged. Cuts output length (and therefore latency) significantly on multi-turn input."}}},"support_threshold":{"type":"string","enum":["low","medium","high","critical"],"description":"Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows."},"include_evidence":{"type":"boolean","description":"Top-level alias for options.include_evidence. When false, the `evidence` array is omitted from the response. Default: true."},"continuation_token":{"type":"string","description":"Opaque, signed token returned by a previous call to this endpoint. Pass it back here to continue the analysis with prior context — no need to re-send prior_messages. The token IS the state; we store nothing."},"reset_conversation":{"type":"boolean","description":"When true, discard any continuation_token and treat this call as the first turn of a new conversation."},"message_id":{"type":"string","maxLength":128,"description":"Optional customer-side identifier for this turn. If provided, evidence pointers in the continuation_token will reference this id so you can map detections back to your own messages. If omitted we use a deterministic short hash of the text."},"bypass_cache":{"type":"boolean","description":"Deprecated, no effect: results are never cached — every request runs a fresh evaluation and nothing derived from the input is stored. The field is still accepted so existing integrations do not break."},"external_id":{"type":"string","maxLength":255,"description":"Your unique identifier (e.g. message ID, content ID) for correlating Tuteliq results with your own system. Echoed back in the response and included in webhook payloads so you can match alerts to the original content."},"customer_id":{"type":"string","maxLength":255,"description":"Your end-customer identifier for multi-tenant / B2B2C scenarios. If you serve multiple customers (schools, apps, brands) from a single API key, set this to route webhook alerts to the correct customer. Echoed back in the response and included in webhook payloads as \"customerId\"."},"metadata":{"type":"object","additionalProperties":true,"maxProperties":20,"description":"Arbitrary key-value pairs for additional context (e.g. { \"channel\": \"discord\", \"region\": \"eu\" }). Stored with the detection result, echoed in the response, and included in webhook payloads. Maximum 20 properties."},"incident_moderation_enabled":{"type":"boolean","description":"Per-call override of your account incident logging setting. When set, it takes precedence for THIS request: true forces the incident to be persisted, false suppresses persistence. Omit to use your account default (which itself defaults to enabled). Useful to suppress logging for test traffic or to opt specific calls in or out."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"declared_age_overridden":{"type":"boolean","description":"True when the caller declared an adult age group but the speaker states a minor age in the message or in the earlier turns sent with it. The declared age was not used. Nothing is escalated by this on its own; the analysis runs as if no age had been sent. Worth investigating at your end: an age check that disagrees with what the user says is what a bypassed age verification looks like. A stated ADULT age never has this effect. Absent unless it applied."},"declared_age_overridden_reason":{"type":"string","description":"Human-readable explanation. Present only when declared_age_overridden is true."},"urgency_kept":{"type":"boolean","description":"True when a declared `context.conversation_type` would have capped an urgent action to `flag_for_review` and did not, because the case is acute: the analysis named an acute marker, or a separate reading found a person at risk now with something happening as the text is written. A reading that gave no answer also keeps the urgency. Absent otherwise, and never present together with `conversation_type_applied`."},"urgency_kept_reason":{"type":"string","description":"Human-readable explanation. Present only when urgency_kept is true."},"conversation_type_applied":{"type":"string","description":"Present only when a caller-declared `context.conversation_type` actually changed routing. Confirms the setting was taken into account, rather than leaving the platform to assume it was."},"absence_reason_cleared":{"type":"boolean","description":"romance-scam only. Present when a detection was cleared because the reason given for not appearing on video or in person was a passing obstacle (a router down, out of data, an early start) rather than a standing property of the claimed persona (a deployment, a rig, a clearance), and the only tactics were IDENTITY_FABRICATION / SECRECY_REQUEST / URGENCY_PRESSURE. Before this, an ordinary \"no wifi until Thursday\" message was told its sender was fabricating an identity (#224). Absent when the gate did not fire."},"past_account_cleared":{"type":"boolean","description":"romance-scam only. Present when a result that asked for no action was not reported as a detection, because a separate reading found the message is an account of something that is over, or is about nobody in particular (a warning, a lesson, a story about a relative years ago). Never present when the analysis asked for monitoring or review."},"unseen_partner_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a romantic relationship with someone met online and never met in person; that person has never been seen on video or gives a reason they cannot be seen or checked; talk of love, marriage or keeping it private. The result is raised to `flag_for_review`. This pattern is complete before any money is asked for."},"unseen_partner_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when unseen_partner_raised is true."},"love_bombing_raised":{"type":"boolean","description":"Romance scam only. True when the analysis left the result below review and a separate reading found all of: a declaration of lifelong or exclusive devotion to the reader; the message itself showing that the two barely know each other or have not met; and nothing of a shared life in it. The result is raised to review, never higher. This is how a romance scam opens, before anything is asked for, and it can also be sincere, so a moderator decides. Absent otherwise. ADVISORY: for a moderator UI and triage queues. Branch production logic on `recommended_action`."},"love_bombing_raised_reason":{"type":"string","description":"Human-readable explanation. Present only when love_bombing_raised is true."},"ordinary_shared_finances_capped":{"type":"boolean","description":"Romance scam only. True when, across a whole conversation (earlier turns submitted, or a conversation pasted into `text`), a separate reading found that money moves the way it does between people who share a life (a cost split, a sum covered and paid back, a shared expense), with no unmet or unverifiable partner, unusual payment method, crisis, secrecy or promised returns. A result at review or block is then brought down to `monitor`. Only `recommended_action` changes; `detected`, the categories, the level and the score are as the analysis gave them. Never applied to a single message, a declared minor, a kept urgency, or a result another reading cleared or raised. Absent otherwise."},"ordinary_shared_finances_capped_reason":{"type":"string","description":"Human-readable explanation. Present only when ordinary_shared_finances_capped is true."},"protective_speech_capped":{"type":"boolean","description":"True when an alert was lowered because a separate reading found a text that works AGAINST the harm it describes: explaining it, warning about it, counselling or supporting someone, reporting it or asking what to do. Two outcomes: with nobody at risk in the text and the detector itself reading the speaker as describing someone else, the result is nothing detected at `monitor`; with a person at risk and a responsible adult in the text already acting, an urgent action becomes `flag_for_review` and the finding is kept. A text that does the harm, a person at risk with nobody acting, anything happening as the text is written, and any failed reading are never lowered. Absent when not applied."},"protective_speech_capped_reason":{"type":"string","description":"Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true."},"endpoint":{"type":"string","description":"The detection endpoint that produced this result"},"detected":{"type":"boolean","description":"A signal was observed at ANY severity (including low / monitor cases). For production branching prefer `recommended_action` (`flag_for_moderator` / `immediate_intervention` indicate action required)."},"severity":{"type":"number","description":"Severity score (0.0-1.0)"},"confidence":{"type":"number","description":"Model confidence in the classification (0.0-1.0)"},"risk_score":{"type":"number","description":"Age-adjusted risk score (0.0-1.0)"},"level":{"type":"string","enum":["none","low","medium","high","critical"],"description":"Human-readable severity level"},"categories":{"type":"array","items":{"type":"object","properties":{"tag":{"type":"string","description":"Category identifier (e.g., \"URGENCY_FABRICATION\")"},"label":{"type":"string","description":"Human-readable category label"},"confidence":{"type":"number","description":"Confidence for this category (0.0-1.0)"}}},"description":"Detected categories with confidence scores"},"evidence":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"Evidence excerpt from the input"},"tactic":{"type":"string","description":"The tactic or indicator this evidence relates to"},"weight":{"type":"number","description":"Evidence weight (0.0-1.0)"}}},"description":"Evidence excerpts supporting the detection"},"age_calibration":{"type":"object","properties":{"applied":{"type":"boolean"},"age_group":{"type":"string"},"multiplier":{"type":"number"}}},"recommended_action":{"type":"string","description":"Recommended action to take"},"rationale":{"type":"string","description":"Explanation of the detection result"},"message_analysis":{"type":"array","items":{"type":"object","properties":{"message_index":{"type":"number","description":"Index of the message in the conversation history (1-based)"},"risk_score":{"type":"number","description":"Risk score for this individual message (0.0-1.0)"},"flags":{"type":"array","items":{"type":"string"},"description":"Tactics or indicators detected in this message"},"summary":{"type":"string","description":"Brief explanation of this message's risk contribution"}}},"description":"Per-message risk breakdown (only present when prior_messages is provided)"},"language":{"type":"string","description":"Language code used for analysis"},"language_status":{"type":"string","enum":["stable","beta"],"description":"Language support maturity"},"credits_used":{"type":"number","description":"Credits consumed by this request"},"processing_time_ms":{"type":"number","description":"Processing time in milliseconds"},"continuation_token":{"type":"string","description":"Opaque, signed token carrying derived analysis state. Pass it back on the next call to continue the conversation without re-sending history. Server stores nothing — the token IS the state."},"continuation_expires_at":{"type":"string","description":"ISO 8601 timestamp at which the continuation_token expires (24h default)."},"state_source":{"type":"string","enum":["token","fresh","reset"],"description":"How the prior analysis state was sourced for this call: \"token\" (decoded from continuation_token), \"fresh\" (no prior state), \"reset\" (reset_conversation forced a fresh start)."},"external_id":{"type":"string","description":"Echo of the external_id you provided in the request"},"customer_id":{"type":"string","description":"Echo of the customer_id you provided in the request"},"metadata":{"type":"object","additionalProperties":true,"description":"Echo of the metadata you provided in the request"},"support":{"type":"object","description":"Country-specific crisis helplines and response guidance (only for high/critical severity)","properties":{"country":{"type":"string"},"country_name":{"type":"string"},"emergency_number":{"type":"string"},"helplines":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"number":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"available":{"type":"string"}}}},"response_guide":{"type":"object","properties":{"category":{"type":"string"},"immediateActions":{"type":"array","items":{"type":"string"}},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"}}}}}}}},"classification":{"type":"string","enum":["confirmed_synthetic","suspected_synthetic","unknown","confirmed_authentic"],"description":"Synthetic content classification level"}}}}}}}}},"/api/v1/safety/synthetic-content/image":{"post":{"summary":"Detect AI-generated or synthetic images","tags":["Safety"],"description":"Multi-signal forensic analysis: semantic analysis, EXIF metadata, pixel statistics, C2PA Content Credentials, watermark detection, and perceptual hashing. Classifies as confirmed_synthetic, suspected_synthetic, unknown, or confirmed_authentic.","responses":{"200":{"description":"Default Response"}}}},"/api/v1/safety/synthetic-content/audio":{"post":{"summary":"Detect AI-generated or cloned voice audio","tags":["Safety"],"description":"Multi-signal audio analysis: transcription-based text analysis plus spectral analysis (mel spectrogram semantic analysis, audio statistics for TTS indicators). Classifies as confirmed_synthetic, suspected_synthetic, unknown, or confirmed_authentic.","responses":{"200":{"description":"Default Response"}}}},"/api/v1/safety/synthetic-content/video":{"post":{"summary":"Detect deepfake or AI-generated video","tags":["Safety"],"description":"Multi-signal forensic video analysis: per-frame semantic analysis, temporal face consistency, lip-sync correlation, audio spectral analysis, and transcription. Classifies as confirmed_synthetic, suspected_synthetic, unknown, or confirmed_authentic.","responses":{"200":{"description":"Default Response"}}}},"/api/v1/safety/csam":{"post":{"summary":"CSAM risk triage for an image","tags":["Safety"],"description":"Compositional CSAM risk triage: age estimation, anatomical and skin-exposure signals, scene context, and known-material hash matching where the account is entitled. Returns a review-priority band with the evidence behind it, plus whether the material appears synthetic, which changes how it should be escalated. Produces review priorities for trained human reviewers; it does not adjudicate, report or remove content.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"endpoint":{"type":"string"},"risk_level":{"type":"string","description":"Review priority band"},"risk_score":{"type":"number","description":"Composite score, 0 to 1"},"requires_escalation":{"type":"boolean"},"requires_review":{"type":"boolean"},"recommendation":{"type":"string","description":"What a reviewer should do next"},"sources":{"type":"array","items":{"type":"string"},"description":"Detection layers that ran"},"signals":{"type":"array","items":{"type":"string"}},"benign_context":{"type":"boolean","description":"Benign skin context detected (beach, bath, pool)"},"age_signal":{"type":"object","properties":{"minor_detected":{"type":"boolean"},"child_detected":{"type":"boolean"},"youngest_age":{"type":["number","null"]},"youngest_age_group":{"type":["string","null"]},"face_count":{"type":"number"}}},"nudity_signal":{"type":"object","properties":{"is_explicit":{"type":"boolean"},"is_suggestive":{"type":"boolean","description":"Minimal or revealing attire. NOT a sexualisation signal; do not treat it as one."},"nsfw_score":{"type":"number"},"skin_exposure_ratio":{"type":"number"},"intimate_region_score":{"type":"number"}}},"synthetic_signal":{"type":"object","description":"Whether the material appears synthetic, which changes the escalation path","properties":{"is_synthetic":{"type":"boolean"},"classification":{"type":["string","null"]},"calibrated_confidence":{"type":["number","null"]}}},"engine":{"type":"string","description":"Which implementation produced the assessment"},"processing_time_ms":{"type":"number"},"disclaimer":{"type":"string"}}}}}}}}},"/api/v1/safety/risk-trends":{"get":{"summary":"Get anonymised global risk trends","tags":["Risk Profile"],"description":"Returns aggregated, fully anonymised risk signal trends across all customers. No PII is stored or returned. Available to Business tier and above.","parameters":[{"schema":{"type":"number","minimum":1,"maximum":90,"default":30},"in":"query","name":"days","required":false,"description":"Number of days to look back (max 90)"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"trends":{"type":"object","additionalProperties":true}}}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"error":{"type":"string"}}}}}}}}},"/api/v1/intelligence/trends":{"get":{"summary":"Get anonymised threat intelligence trends","tags":["Threat Intelligence"],"description":"Returns aggregated, anonymised detection trends across all customers. Includes breakdowns by endpoint, category, age group, platform, and language. All data is k-anonymised — buckets with fewer than 5 signals are suppressed. No PII or customer-identifiable information is ever included.","parameters":[{"schema":{"type":"string"},"in":"query","name":"days","required":false,"description":"Number of days to analyse (default 30, max 90)"},{"schema":{"type":"string"},"in":"query","name":"endpoint","required":false,"description":"Filter by detection endpoint (e.g. \"bullying\", \"grooming\")"},{"schema":{"type":"string"},"in":"query","name":"age_group","required":false,"description":"Filter by age group (e.g. \"under_13\", \"13_17\")"},{"schema":{"type":"string"},"in":"query","name":"platform","required":false,"description":"Filter by platform (e.g. \"discord\", \"instagram\")"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}}},"total_signals":{"type":"number"},"trends_by_endpoint":{"type":"array","items":{"type":"object","additionalProperties":true}},"trends_by_category":{"type":"array","items":{"type":"object","additionalProperties":true}},"trends_by_age_group":{"type":"array","items":{"type":"object","additionalProperties":true}},"trends_by_platform":{"type":"array","items":{"type":"object","additionalProperties":true}},"emerging_threats":{"type":"array","items":{"type":"object","additionalProperties":true}},"geographic_trends":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/v1/intelligence/emerging":{"get":{"summary":"Get emerging threats","tags":["Threat Intelligence"],"description":"Identifies threat categories with rapid growth (>50% week-over-week increase). Useful for early warning systems and proactive safety measures. All data is anonymised and k-anonymity is enforced.","parameters":[{"schema":{"type":"string"},"in":"query","name":"days","required":false,"description":"Comparison window in days (default 7)"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"period_days":{"type":"number"},"emerging_threats":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/v1/intelligence/weekly-digest":{"get":{"summary":"Get weekly threat intelligence digest","tags":["Threat Intelligence"],"description":"Returns a pre-formatted weekly summary suitable for email or Slack notifications. Includes top threats, emerging categories, and notable trends. All data is anonymised with no PII.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"generated_at":{"type":"string"},"period":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"}}},"summary":{"type":"string"},"total_signals":{"type":"number"},"top_endpoints":{"type":"array","items":{"type":"object","additionalProperties":true}},"top_categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"emerging_threats":{"type":"array","items":{"type":"object","additionalProperties":true}},"notable_changes":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/v1/policy/automation/":{"get":{"summary":"List all policy rules","tags":["Policy Rules"],"description":"Returns all policy rules for the authenticated user, sorted by priority (ascending).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"rules":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"string"}},"conditions":{"type":"object","properties":{"min_risk_score":{"type":"number","minimum":0,"maximum":1},"min_severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_groups":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"action":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["block","flag","escalate","notify","log_only"]},"escalate_to":{"type":"string"},"notify_channel":{"type":"string"},"message":{"type":"string"},"override_severity":{"type":"string"}},"additionalProperties":false},"priority":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}}}}}},"post":{"summary":"Create a policy rule","tags":["Policy Rules"],"description":"Create a new declarative policy rule. Rules are evaluated during detection and the highest-priority matching rule determines the policy_action in the response.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","enabled","endpoints","conditions","action","priority"],"properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"string"},"minItems":1},"conditions":{"type":"object","properties":{"min_risk_score":{"type":"number","minimum":0,"maximum":1},"min_severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_groups":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"action":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["block","flag","escalate","notify","log_only"]},"escalate_to":{"type":"string"},"notify_channel":{"type":"string"},"message":{"type":"string"},"override_severity":{"type":"string"}},"additionalProperties":false},"priority":{"type":"integer","minimum":0}},"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"rule":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"string"}},"conditions":{"type":"object","properties":{"min_risk_score":{"type":"number","minimum":0,"maximum":1},"min_severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_groups":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"action":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["block","flag","escalate","notify","log_only"]},"escalate_to":{"type":"string"},"notify_channel":{"type":"string"},"message":{"type":"string"},"override_severity":{"type":"string"}},"additionalProperties":false},"priority":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}}}}}},"/api/v1/policy/automation/{ruleId}":{"get":{"summary":"Get a policy rule","tags":["Policy Rules"],"description":"Retrieve a specific policy rule by ID.","parameters":[{"schema":{"type":"string"},"in":"path","name":"ruleId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"rule":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"string"}},"conditions":{"type":"object","properties":{"min_risk_score":{"type":"number","minimum":0,"maximum":1},"min_severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_groups":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"action":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["block","flag","escalate","notify","log_only"]},"escalate_to":{"type":"string"},"notify_channel":{"type":"string"},"message":{"type":"string"},"override_severity":{"type":"string"}},"additionalProperties":false},"priority":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}}}}},"put":{"summary":"Update a policy rule","tags":["Policy Rules"],"description":"Update an existing policy rule. Partial updates are supported.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"string"},"minItems":1},"conditions":{"type":"object","properties":{"min_risk_score":{"type":"number","minimum":0,"maximum":1},"min_severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_groups":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"action":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["block","flag","escalate","notify","log_only"]},"escalate_to":{"type":"string"},"notify_channel":{"type":"string"},"message":{"type":"string"},"override_severity":{"type":"string"}},"additionalProperties":false},"priority":{"type":"integer","minimum":0}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"ruleId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"rule":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"enabled":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"string"}},"conditions":{"type":"object","properties":{"min_risk_score":{"type":"number","minimum":0,"maximum":1},"min_severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_groups":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},"action":{"type":"object","required":["type"],"properties":{"type":{"type":"string","enum":["block","flag","escalate","notify","log_only"]},"escalate_to":{"type":"string"},"notify_channel":{"type":"string"},"message":{"type":"string"},"override_severity":{"type":"string"}},"additionalProperties":false},"priority":{"type":"integer"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}}}}}}},"delete":{"summary":"Delete a policy rule","tags":["Policy Rules"],"description":"Permanently delete a policy rule.","parameters":[{"schema":{"type":"string"},"in":"path","name":"ruleId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}}}}},"/api/v1/policy/automation/evaluate":{"post":{"summary":"Test-evaluate policy rules","tags":["Policy Rules"],"description":"Evaluate your policy rules against a mock detection result. Useful for testing rules before going live.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["endpoint","risk_score","severity","categories"],"properties":{"endpoint":{"type":"string"},"risk_score":{"type":"number","minimum":0,"maximum":1},"severity":{"type":"string","enum":["low","medium","high","critical"]},"categories":{"type":"array","items":{"type":"string"}},"age_group":{"type":"string"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"evaluation":{"type":"object","properties":{"rules_evaluated":{"type":"integer"},"rules_matched":{"type":"array","items":{"type":"object","properties":{"rule_id":{"type":"string"},"rule_name":{"type":"string"},"action":{"type":"string"},"priority":{"type":"integer"}}}},"policy_action":{"type":"string"},"policy_message":{"type":"string"},"applied_rule":{"type":"string"}}}}}}}}}}},"/api/v1/settings/detection":{"get":{"summary":"Get detection settings","tags":["Settings"],"description":"Get your account-level detection settings. These control which detection endpoints are active for your account. Disabled endpoints are automatically skipped in /analyse/multi and return not_detected on individual calls.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"enabled_endpoints":{"type":"array","items":{"type":"string","enum":["bullying","grooming","unsafe","social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","synthetic-content"]},"description":"Only these endpoints will run (whitelist mode)"},"disabled_endpoints":{"type":"array","items":{"type":"string","enum":["bullying","grooming","unsafe","social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","synthetic-content"]},"description":"These endpoints are skipped (blacklist mode)"},"default_context":{"type":"object","properties":{"age_group":{"type":"string"},"platform":{"type":"string"},"country":{"type":"string"}}},"default_conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Account-level default for `context.conversation_type`. A request that sets `context.conversation_type` explicitly always overrides this. No default value: leave unset for unchanged behaviour. Set from your own platform configuration, never from end-user input."},"default_flag_profanity":{"type":"boolean","description":"Account-level default for options.flag_profanity on /bullying and /unsafe. A request that sets options.flag_profanity explicitly always overrides this."},"default_flag_risk_terms":{"type":"boolean","description":"Account-level default for options.flag_risk_terms on /bullying and /unsafe. A request that sets options.flag_risk_terms explicitly always overrides this."},"available_endpoints":{"type":"array","items":{"type":"string"},"description":"All available detection endpoints"},"updated_at":{"type":"string"}}}}}}}},"put":{"summary":"Update detection settings","tags":["Settings"],"description":"Configure which detection endpoints are active for your account. Use `enabled_endpoints` (whitelist — only these run) OR `disabled_endpoints` (blacklist — these are skipped). Cannot use both.\n\nExample for an adult platform that only needs self-harm detection:\n```json\n{\"enabled_endpoints\": [\"distress-signals\", \"unsafe\"]}\n```\n\nExample to disable grooming and bullying but keep everything else:\n```json\n{\"disabled_endpoints\": [\"grooming\", \"bullying\"]}\n```","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled_endpoints":{"type":"array","items":{"type":"string","enum":["bullying","grooming","unsafe","social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","synthetic-content"]},"description":"Only these endpoints will run. Mutually exclusive with disabled_endpoints."},"disabled_endpoints":{"type":"array","items":{"type":"string","enum":["bullying","grooming","unsafe","social-engineering","app-fraud","romance-scam","mule-recruitment","gambling-harm","coercive-control","vulnerability-exploitation","radicalisation","distress-signals","tfgbv","synthetic-content"]},"description":"These endpoints are skipped. Mutually exclusive with enabled_endpoints."},"default_context":{"type":"object","properties":{"age_group":{"type":"string","description":"Default age group (e.g. \"adult\", \"13-17\")"},"platform":{"type":"string","description":"Default platform name"},"country":{"type":"string","description":"Default ISO country code"}}},"default_conversation_type":{"type":"string","enum":["fraud_report","abuse_report","support_disclosure","helpline","clinical","investigative","legal","advisory","training","journalism","research","moderation_review","healthcare","law_enforcement","education","family","workplace","dating","adult_community","direct_message","group_chat","public_post","customer_service","gaming_chat"],"description":"Account-level default for `context.conversation_type`. A request that sets `context.conversation_type` explicitly always overrides this. No default value: leave unset for unchanged behaviour. Set from your own platform configuration, never from end-user input."},"default_flag_profanity":{"type":"boolean","description":"Account-level default for options.flag_profanity on /bullying and /unsafe. A request that sets options.flag_profanity explicitly always overrides this."},"default_flag_risk_terms":{"type":"boolean","description":"Account-level default for options.flag_risk_terms on /bullying and /unsafe. A request that sets options.flag_risk_terms explicitly always overrides this."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"settings":{"type":"object","additionalProperties":true},"message":{"type":"string"}}}}}}}},"delete":{"summary":"Reset detection settings to defaults","tags":["Settings"],"description":"Remove all custom detection settings. All endpoints will be active.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"message":{"type":"string"}}}}}}}}},"/api/v1/audit/receipts/{request_id}":{"get":{"summary":"Fetch an EU AI Act Art 12 audit receipt","tags":["Audit"],"description":"Returns the signed audit receipt for a past inference. Only the deployer that produced the receipt can fetch it (api-key fingerprint match enforced; non-matches return 404 to avoid existence leaks). Receipts are signature-verified before return; tampered storage triggers a 500 with code AUDIT_RECEIPT_FAILED.","parameters":[{"schema":{"type":"string","maxLength":128},"in":"path","name":"request_id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"payload":{"type":"object","description":"The signed payload. Inputs were not stored — only their hash.","additionalProperties":true},"signature":{"type":"object","properties":{"algorithm":{"type":"string"},"key_id":{"type":"string"},"signature":{"type":"string"},"signed_at":{"type":"string"}}},"canonical":{"type":"string","description":"The canonical JSON bytes that were signed (use for out-of-band verification)."}}}}}}}}},"/api/v1/incidents/{incident_id}/review":{"post":{"summary":"Submit a moderator review of an incident (Art 14 human oversight)","tags":["Incidents"],"description":"Records a deployer-side moderator decision (confirm / downgrade / escalate / reclassify / dismiss) on a model-produced incident. The override is persisted to the incident document and a separate signed Art 12 audit receipt is emitted linking back to the original. The optional reason_comment is encrypted with the customer's registered public key (end-to-end) when enabled; otherwise server-side AES.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","reason_code"],"properties":{"action":{"type":"string","enum":["confirm","downgrade","escalate","reclassify","dismiss","resolve","reopen"]},"reason_code":{"type":"string","enum":["confirmed_accurate","false_positive","out_of_context","insufficient_severity","incorrect_category","requires_law_enforcement","parent_notified","other"]},"reason_comment":{"type":"string","maxLength":4000},"new_risk_level":{"type":"string","enum":["low","moderate","high","critical"]},"new_risk_category":{"type":"string","enum":["bullying","grooming","unsafe","self_harm","other","social_engineering","app_fraud","romance_scam","mule_recruitment","gambling_harm","coercive_control","vulnerability_exploitation","radicalisation","distress_signals","tfgbv","synthetic_content"]},"moderator_external_id":{"type":"string","maxLength":256},"retention_class":{"type":"string","enum":["biometric-high-risk","safety-high-risk","limited-risk","minimal-risk"]}}}}}},"parameters":[{"schema":{"type":"string","maxLength":128},"in":"path","name":"incident_id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"incident_id":{"type":"string"},"original":{"type":"object","properties":{"risk_category":{"type":"string"},"risk_level":{"type":"string"}}},"revised":{"type":"object","properties":{"risk_category":{"type":"string"},"risk_level":{"type":"string"}}},"audit_receipt":{"type":"object","properties":{"request_id":{"type":"string"},"timestamp":{"type":"string"},"signature":{"type":"string"}}}}}}}}}}},"/api/v1/incidents/batch-review":{"post":{"summary":"Submit the same moderator review across multiple incidents (Art 14, bulk)","tags":["Incidents"],"description":"Applies the same review action + reason to up to 100 incidents in one call. Each incident emits its own signed Art 12 audit receipt — bulk is a UX shortcut, not a compliance shortcut. Per-incident success/error is returned so a partial failure can be retried without re-running successful items.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["incident_ids","action","reason_code"],"properties":{"incident_ids":{"type":"array","items":{"type":"string","maxLength":128},"minItems":1,"maxItems":100},"action":{"type":"string","enum":["confirm","downgrade","escalate","reclassify","dismiss","resolve","reopen"]},"reason_code":{"type":"string","enum":["confirmed_accurate","false_positive","out_of_context","insufficient_severity","incorrect_category","requires_law_enforcement","parent_notified","other"]},"reason_comment":{"type":"string","maxLength":4000},"new_risk_level":{"type":"string","enum":["low","moderate","high","critical"]},"new_risk_category":{"type":"string","enum":["bullying","grooming","unsafe","self_harm","other","social_engineering","app_fraud","romance_scam","mule_recruitment","gambling_harm","coercive_control","vulnerability_exploitation","radicalisation","distress_signals","tfgbv","synthetic_content"]},"moderator_external_id":{"type":"string","maxLength":256},"retention_class":{"type":"string","enum":["biometric-high-risk","safety-high-risk","limited-risk","minimal-risk"]}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}}},"/api/v1/incidents/":{"get":{"summary":"List the caller's incidents (newest first, paginated)","tags":["Incidents"],"description":"Read-only paginated listing of incidents recorded under this API key's account. Each row is non-PII metadata by default; pass `include_summary=true` to include the decrypted incident summary (extra credits per row). BYOK incidents return the hybrid envelope on the summary field and list the field name in `_e2e_envelope_fields`.","parameters":[{"schema":{"type":"string"},"in":"query","name":"category","required":false,"description":"Filter by risk_category (e.g. \"bullying\", \"grooming\")."},{"schema":{"type":"string","enum":["low","moderate","high","critical","medium"]},"in":"query","name":"severity","required":false,"description":"Filter by risk_level: low, moderate, high, critical. `medium` is accepted as another spelling of `moderate` and returns the same incidents; they are returned with risk_level `moderate`."},{"schema":{"type":"string","enum":["new","reviewed","escalated","resolved","dismissed","reviewing"]},"in":"query","name":"status","required":false,"description":"Filter by status: new, reviewed, escalated, resolved, dismissed. `reviewing` is deprecated: it is still accepted so existing calls do not fail, but it is not a status an incident can have and returns no incidents. Use `new` for incidents nobody has acted on and `reviewed` for ones a moderator has."},{"schema":{"type":"string","enum":["text","voice","image","video","video_stream"]},"in":"query","name":"source","required":false,"description":"Filter by source modality."},{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"ISO 8601 inclusive lower bound on created_at."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"ISO 8601 exclusive upper bound on created_at."},{"schema":{"type":"string"},"in":"query","name":"platform","required":false},{"schema":{"type":"string"},"in":"query","name":"external_id","required":false},{"schema":{"type":"string"},"in":"query","name":"customer_id","required":false},{"schema":{"type":"string"},"in":"query","name":"limit","required":false,"description":"1..100, default 25."},{"schema":{"type":"string"},"in":"query","name":"cursor","required":false,"description":"Opaque next_cursor from the previous page."},{"schema":{"type":"string"},"in":"query","name":"include_summary","required":false,"description":"Set \"true\" to include decrypted summary text on each row (extra credits per row)."}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/incidents/overview":{"get":{"summary":"KPI overview of incidents in a time window","tags":["Incidents"],"description":"Counts by category, severity, source, and status, plus top platforms and 24h/7d/30d totals. Defaults to the last 30 days. Read-only; no LLM call.","parameters":[{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"ISO 8601 inclusive lower bound; default = now - 30d."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"ISO 8601 exclusive upper bound; default = now."}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/incidents/trends":{"get":{"summary":"Time-series buckets of incident volume","tags":["Incidents"],"description":"Returns time-bucketed counts of incidents (with per-bucket severity breakdown). Bucket sizes: hour, day (default), week. Useful for rendering trend charts in dashboards.","parameters":[{"schema":{"type":"string","enum":["hour","day","week"]},"in":"query","name":"bucket","required":false,"description":"Bucket size; default = day."},{"schema":{"type":"string"},"in":"query","name":"from","required":false,"description":"ISO 8601 inclusive lower bound; default = now - 30d."},{"schema":{"type":"string"},"in":"query","name":"to","required":false,"description":"ISO 8601 exclusive upper bound; default = now."}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/incidents/{incident_id}":{"get":{"summary":"Get the full detail for a single incident (read-only)","tags":["Incidents"],"description":"Returns the full incident document for the caller's account. Server-encrypted fields (summary, metadata, source_data) are decrypted server-side. BYOK fields are returned as hybrid envelopes for client-side decryption — see `_e2e_envelope_fields` in the response for which to decrypt locally. 404 if the incident does not exist or does not belong to this account.","parameters":[{"schema":{"type":"string","maxLength":128},"in":"path","name":"incident_id","required":true}],"responses":{"200":{"description":"Default Response"}}}},"/api/v1/agent/check-in":{"post":{"summary":"Report a device attestation (endpoint agent check-in)","tags":["Agent"],"description":"The tuteliq-agent CLI evaluates the org's device-safety policy locally and reports the pass/fail result here. One latest-state record is kept per device (upsert, no history). The org is derived from the authenticated API key — any org_id in the body is ignored. No user content is stored; only device posture (policy rule results, versions, platform).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device_id","agent_version","platform","policy_id","passed","results","attested_at"],"properties":{"device_id":{"type":"string","maxLength":128,"description":"Stable non-PII device identifier (agent-generated hash)."},"agent_version":{"type":"string","maxLength":32,"description":"Reporting agent version (e.g. \"0.1.0\")."},"platform":{"type":"string","maxLength":32,"description":"OS platform (e.g. \"darwin\", \"win32\", \"linux\")."},"policy_id":{"type":"string","maxLength":128,"description":"Identifier of the evaluated policy manifest."},"passed":{"type":"boolean","description":"True iff every policy rule passed."},"results":{"type":"array","maxItems":100,"items":{"type":"object","required":["rule","passed"],"properties":{"rule":{"type":"object","additionalProperties":true,"description":"The evaluated policy rule (posture check — process/file/version/OS). Never contains user content."},"passed":{"type":"boolean"},"detail":{"type":"string","maxLength":512,"description":"Human-readable reason the rule failed (omitted on pass)."}}},"description":"Per-rule evaluation results."},"attested_at":{"type":"string","maxLength":40,"description":"ISO 8601 timestamp of the local evaluation."},"org_id":{"type":"string","maxLength":128,"description":"Ignored — the org is always derived from the API key. Never trusted from the body."}}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"recorded":{"type":"boolean"},"expires_at":{"type":"string","description":"ISO 8601 expiry (attested_at + 90 days); Firestore TTL reaps the row after this."},"credits_used":{"type":"number"}}}}}}}}},"/api/v1/agent/fleet":{"get":{"summary":"List the org's device fleet with a compliance summary","tags":["Agent"],"description":"Returns every device's latest attestation for the caller's org (newest first) plus a rollup summary (total / compliant / non-compliant / compliance %). Read-only, deterministic, no LLM. Scoped to the API key's org.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"type":"object","additionalProperties":true}},"summary":{"type":"object","properties":{"total":{"type":"number"},"compliant":{"type":"number"},"non_compliant":{"type":"number"},"compliance_pct":{"type":"number"}}},"credits_used":{"type":"number"}}}}}}}}},"/api/v1/agent/fleet/{deviceId}":{"get":{"summary":"Get a single device's latest attestation","tags":["Agent"],"description":"Returns the latest attestation for one device in the caller's org. 404 if the device has never checked in (or belongs to another org).","parameters":[{"schema":{"type":"string","maxLength":128},"in":"path","name":"deviceId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"device":{"type":"object","additionalProperties":true},"credits_used":{"type":"number"}}}}}}}}}},"servers":[{"url":"https://api.tuteliq.ai","description":"Production server"},{"url":"http://localhost:3000","description":"Development server"}],"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"tags":[{"name":"Safety","description":"Core detection endpoints for all nine KOSA harm categories. Supports text, voice, and image input with age-calibrated severity scoring."},{"name":"Fraud","description":"Financial and social fraud detection — social engineering, app fraud, romance scams, and money mule recruitment. Identifies tactics targeting minors and vulnerable users with evidence-based scoring."},{"name":"Safety Extended","description":"Extended safety detection — gambling harm, coercive control, vulnerability exploitation with cross-endpoint modifiers, and radicalisation. Requires Indie tier or above."},{"name":"Analyse","description":"Multi-endpoint analysis. Fan-out a single text to up to 10 detection endpoints in parallel with aggregated results, vulnerability modifier support, and per-endpoint breakdowns."},{"name":"Analysis","description":"Emotional intelligence endpoints. Analyze conversations for dominant emotions, sentiment trends (improving/stable/worsening), and early indicators of depression or anxiety. Designed to surface mental health risks before they escalate."},{"name":"Guidance","description":"Post-detection action plans tailored by audience. Generate age-appropriate guidance for children, parents, or platform trust & safety teams — complete with reading-level calibration and tone adjustment. Goes beyond \"here's a risk score\" to answer \"what do we do about it?\""},{"name":"Reports","description":"Professional incident report generation for schools, counselors, and moderators. Converts raw conversation data into structured reports with risk levels, categorized findings, and recommended next steps — ready for handoff to human reviewers."},{"name":"Batch","description":"Analyze up to 50 items in a single request with optional parallel processing. Supports all analysis types (bullying, grooming, unsafe, emotions). Built for production pipelines that need to process backlogs or moderate content in bulk."},{"name":"Webhooks","description":"Real-time notification endpoints for receiving alerts when critical incidents are detected. Full CRUD management with HMAC-SHA256 signed payloads, automatic retry on failure, secret regeneration, and test delivery — everything you need for production event-driven architectures."},{"name":"Usage","description":"API usage tracking, quota monitoring, and rate limit status. View daily summaries, historical trends, per-tool breakdowns, and monthly billing period usage. Includes upgrade recommendations when approaching limits."},{"name":"Policy","description":"Customize detection behavior for your use case. Configure sensitivity thresholds, category weights, and moderation rules without changing your integration code."},{"name":"Pricing","description":"Public pricing plan information. Browse available tiers, features, and limits. The public endpoint requires no authentication; detailed plan info requires an API key."},{"name":"Account","description":"GDPR-compliant account data management. Exercise the Right to Erasure (Article 17), Right to Data Portability (Article 20), and Right to Rectification (Article 16). Manage consent records and access your full audit trail. Available to all tiers — privacy is not a premium feature."},{"name":"Compliance","description":"Public transparency endpoints for GDPR compliance. Machine-readable Data Processing Agreement (DPA), current sub-processor list, and data retention schedules. No authentication required — anyone can verify our data practices."},{"name":"Admin","description":"Administrative endpoints for breach management and data retention. Log, track, and manage data breach incidents with full audit trails and notification status tracking. Trigger manual data retention cleanup when needed."},{"name":"Health","description":"Health check and monitoring endpoints. Liveness and readiness probes for Kubernetes/Cloud Run, full dependency health checks, and detailed component status for debugging."},{"name":"Status","description":"Public API status and uptime monitoring. Component-level health (API, database, cache, AI engine), uptime percentages, and an embeddable status page. Use /status/ping for external monitoring services like UptimeRobot."}],"externalDocs":{"description":"KOSA Coverage Documentation","url":"https://tuteliq.ai/docs/kosa-compliance"}}