---
title: "Detect bullying, harassment, and intimidation in text"
method: POST
path: "/api/v1/safety/bullying"
tags: ["Safety"]
---

# Detect bullying, harassment, and intimidation in text

`POST /api/v1/safety/bullying`

## Request

- Base URL: `https://api.tuteliq.ai`
- URL: `https://api.tuteliq.ai/api/v1/safety/bullying`
- Auth: one of:
  - HTTP bearer
  - API key in header `x-api-key`

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.

## Request body

- object
  - `text` string, required: The message text to analyze
  - `context` object
    - `language` string: 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` string: Age group (e.g., "11-13")
    - `age_group` string: Alias for `ageGroup`, as the other detectors spell it. When both are given, `ageGroup` is used.
    - `relationship` string: Relationship between participants
    - `platform` string: Platform name (e.g., "Discord", "Telegram")
    - `conversation_type` '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': 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` object
    - `support_threshold` 'low' | 'medium' | 'high' | 'critical': Minimum severity to show crisis support resources (default: high). Critical always shows.
    - `verdict_only` boolean: 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` boolean: 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` boolean: 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` 'low' | 'medium' | 'high' | 'critical': Top-level alias for options.support_threshold. Minimum severity to show crisis support resources (default: high). Critical always shows.
  - `continuation_token` string: Opaque signed token from a previous /bullying call. Carries derived analysis state — no message content is stored server-side.
  - `reset_conversation` boolean: Discard any continuation_token and start a fresh conversation.
  - `message_id` string: Optional customer-side message id for evidence pointers.
  - `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.
  - `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".
  - `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`

Default Response

- object
  - `speaker_role` 'enacting' | 'experiencing' | 'reporting_other' | 'self_directed' | 'unclear': 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` boolean: 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` string[]: Canonical vocabulary. Values outside this enum are dropped before the response is built, so this field is safe to switch on.
  - `confidence` number
  - `severity` 'low' | 'medium' | 'high' | 'critical'
  - `rationale` string
  - `recommended_action` 'none' | 'monitor' | 'flag_for_review' | 'block' | 'immediate_intervention': Routing key, weakest to strongest: `none`, `monitor`, `flag_for_review`, `block`, `immediate_intervention`. Safe to switch on.
  - `action_detail` string: Optional human-readable expansion of `recommended_action` for a moderator UI. Free text: do not branch on it.
  - `risk_score` number
  - `language` string: Language code used for analysis (auto-detected or provided)
  - `language_status` 'stable' | 'beta': Language support maturity: "stable" (en) or "beta" (all others)
  - `credits_used` number: Number of credits consumed by this request
  - `continuation_token` string: Signed token carrying derived analysis state for the next call. Server stores nothing.
  - `continuation_expires_at` string: ISO 8601 expiry timestamp of the continuation_token.
  - `state_source` 'token' | 'fresh' | 'reset': How prior state was sourced.
  - `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` 'rising' | 'stable' | 'declining' | 'none': Direction of travel across the conversation so far.
  - `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.
  - `escalation_capped` boolean: 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` string: Human-readable explanation of the cap. Present only when escalation_capped is true.
  - `urgency_kept` boolean: 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` string: Human-readable explanation. Present only when urgency_kept is true.
  - `passing_remark_capped` boolean: 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` string: Human-readable explanation. Present only when passing_remark_capped is true.
  - `single_insult_watched` boolean: 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` string: Human-readable explanation. Present only when single_insult_watched is true.
  - `bullying_report_surfaced` boolean: 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` string: Human-readable explanation. Present only when bullying_report_surfaced is true.
  - `rumour_spread_raised` boolean: 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` string: Human-readable explanation. Present only when rumour_spread_raised is true.
  - `looks_attack_raised` boolean: 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` string: Human-readable explanation. Present only when looks_attack_raised is true.
  - `continued_after_objection_raised` boolean: 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` string: Human-readable explanation. Present only when continued_after_objection_raised is true.
  - `declared_age_overridden` boolean: 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` string: Human-readable explanation. Present only when declared_age_overridden is true.
  - `protective_speech_capped` boolean: 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` string: Human-readable explanation, naming which of the two outcomes applied. Present only when protective_speech_capped is true.
  - `number_coincidence_cleared` boolean: 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` string: Human-readable explanation. Present only when number_coincidence_cleared is true.
  - `profanity` object, nullable: 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.
    - `detected` boolean
    - `matches` string[]
  - `risk_terms` object, nullable: 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.
    - `detected` boolean
    - `matches` object[]
      - `term` string
      - `category` 'drug' | 'violence'
  - `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)
    - `country` string
    - `country_name` string
    - `emergency_number` string
    - `helplines` object[]
      - `name` string
      - `number` string
      - `description` string
      - `category` string
      - `available` string
    - `response_guide` object
      - `category` string
      - `immediateActions` string[]
      - `resources` object[]
        - `name` string
        - `description` string
        - `url` string

## Changes

- **2026-10-19** `8490332e0031`: 22 warning, 41 info
  - removed the optional property `normalized` from the response with the `200` status
  - added the new `block` enum value to the `recommended_action` response property for the response status `200`
  - added the new `body_shaming` enum value to the `bullying_type/items/` response property for the response status `200`
  - added the new `coded_harassment` enum value to the `bullying_type/items/` response property for the response status `200`
  - …59 more

[Operation history](https://skmtc.dev/tuteliq/apis/tuteliq-api/changes/api/v1/safety/bullying/post.md)

---

[API](https://skmtc.dev/tuteliq/apis/tuteliq-api.md) · [All operations](https://skmtc.dev/tuteliq/apis/tuteliq-api/llms.txt) · [OpenAPI document](https://skmtc.dev/tuteliq/apis/tuteliq-api/revisions/8490332e0031?raw)
