Skip to main content
POST
Detect grooming patterns in conversations

Authorizations

Authorization
string
header
required

API key as Bearer token

Body

application/json
messages
object[]
required

Sequence of messages to analyze. Consecutive same-role messages of ≤3 characters are automatically defragmented before tactic analysis to prevent letter-by-letter evasion.

Maximum array length: 100
continuation_token
string

Opaque signed token from a previous /grooming call. Carries derived analysis state across calls without server-side storage.

reset_conversation
boolean

Discard any continuation_token and start a fresh conversation.

message_id
string

Optional customer-side identifier for this batch (used as the evidence pointer in the issued continuation_token).

Maximum string length: 128
context
object
options
object
support_threshold
enum<string>

Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows.

Available options:
low,
medium,
high,
critical
external_id
string

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.

Maximum string length: 255
customer_id
string

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".

Maximum string length: 255
metadata
object

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
boolean

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.

Response

200 - application/json

Default Response

conversation_type_applied
string

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
enum<string>

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.

Available options:
none,
low,
medium,
high,
critical
verdict_withdrawn
boolean

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
string

Human-readable explanation. Present only when verdict_withdrawn is true.

verdict_capped
boolean

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. Never fires on a finding that names a tactic. Only ever lowers, never raises. Absent unless it applied.

verdict_capped_reason
string

Human-readable explanation. Present only when verdict_capped is true.

declared_age_overridden
boolean

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
string

Human-readable explanation. Present only when declared_age_overridden is true.

confidence
number
flags
enum<string>[]

Closed vocabulary. Values outside this list are dropped before the response is built, so this field is safe to switch on. Two groups share the array. The eight TACTICS the analyser classifies against are secrecy_request, boundary_pushing, photo_request, gift_giving, isolation, flattery, sexual_content and meeting_request; each asserts that a participant performed that behaviour. The remaining four, sexualization, reconnaissance, grooming and sextortion, reach this array from coded-term corroboration and assert something weaker: that the conversation contains a recognised coded term of that kind. If you are reporting on the tactic taxonomy, use the first eight.

Available options:
secrecy_request,
boundary_pushing,
photo_request,
gift_giving,
isolation,
flattery,
sexual_content,
meeting_request,
sexualization,
reconnaissance,
grooming,
sextortion
confidentiality_frame
enum<string>

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.

Available options:
unconditional_concealment,
limited_confidentiality,
guardian_included,
none
secrecy_frame_cleared
boolean

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
object

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 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
string[]

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
boolean

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.

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.

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.

Available options:
explicit_refusal_overridden,
threatened_exposure
rationale
string
risk_score
number

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.

Available options:
none,
monitor,
flag_for_review,
block,
immediate_intervention
action_detail
string

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
object[]

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.

language
string

Language code used for analysis (auto-detected or provided)

language_status
enum<string>

Language support maturity: "stable" (en) or "beta" (all others)

Available options:
stable,
beta
credits_used
number

Number of credits consumed by this request

continuation_token
string

Privacy-first signed token carrying derived analysis state across calls. Pass back on the next /grooming request.

continuation_expires_at
string

ISO 8601 expiry timestamp of the continuation_token.

state_source
enum<string>

How prior state was sourced for this call.

Available options:
token,
fresh,
reset
trajectory_risk
number

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
enum<string>

Direction of travel across the conversation so far.

Available options:
rising,
stable,
declining,
none
severity_series
number[]

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
enum<string>

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.

Available options:
ok,
engine_error,
out_of_scope_adults
speaker_stance
enum<string>

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 at least one tactic is present: a stance is a statement about a tactic, so it is omitted on a conversation with no flags.

Available options:
deploying,
describing_third_party,
unclear
action_capped
boolean

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. grooming_risk/risk_score are unaffected; only routing was capped. Absent when no cap applied.

action_capped_reason
string

Present only when action_capped is true.

external_id
string

Echo of the external_id you provided in the request

customer_id
string

Echo of the customer_id you provided in the request

metadata
object

Echo of the metadata you provided in the request

support
object

Country-specific crisis helplines and response guidance (only included for high/critical severity)