---
title: "Fan-out analysis across multiple detection endpoints"
method: POST
path: "/api/v1/detection/multi"
tags: ["Analyse"]
---

# Fan-out analysis across multiple detection endpoints

`POST /api/v1/detection/multi`

## Request

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

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.

## Request body

- object
  - `text` string: Text content to analyze
  - `content` string: Alias for text (backward compatibility)
  - `endpoints` string[]: List of endpoint names to run analysis against
  - `detections` string[]: Alias for endpoints (backward compatibility)
  - `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.
    - `age_group` string
    - `ageGroup` string: Alias for age_group (camelCase accepted)
    - `platform` string
    - `country` string: ISO 3166-1 alpha-2 country code
    - `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. When omitted here, the account-level `default_conversation_type` is used if one is set.
    - `prior_messages` object[]: 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.
      - `role` string, required: Role of the sender (e.g., "user", "contact", "adult", "child")
      - `text` string, required: Message text
      - `timestamp` string: ISO 8601 timestamp of the message (optional)
    - `conversation_history` object[]: Deprecated: use prior_messages instead (same shape, same behavior). Kept for backward compatibility.
      - `role` string, required: Role of the sender (e.g., "user", "contact", "adult", "child")
      - `text` string, required: Message text
      - `timestamp` string: ISO 8601 timestamp of the message (optional)
  - `options` object
    - `include_evidence` boolean: Include evidence excerpts in response. Default: true.
    - `support_threshold` 'low' | 'medium' | 'high' | 'critical': Minimum risk level required to include support/helpline resources. Defaults to "high". Critical severity always included regardless of threshold.
    - `flag_profanity` boolean: 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` boolean: 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` 'low' | 'medium' | 'high' | 'critical': Alias for options.support_threshold (top-level convenience field).
  - `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 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.

## Response `200`

Default Response

- object
  - `results` object[]
    - `endpoint` string
    - `detected` boolean
    - `severity` number
    - `confidence` number
    - `risk_score` number
    - `level` string
    - `categories` object[]
    - `evidence` object[]
    - `recommended_action` string
    - `rationale` string
    - `third_party_concern` boolean: 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` string: Human-readable explanation. Present only when third_party_concern is true.
    - `speaker_role` string: 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` string: 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` boolean: Grooming result: the verdict was capped. `verdict_capped_reason` says why. Absent when no cap applied.
    - `verdict_capped_reason` string
    - `verdict_withdrawn` boolean: Grooming result: the verdict was withdrawn as unsupported. `verdict_withdrawn_reason` says why. Absent otherwise.
    - `verdict_withdrawn_reason` string
    - `disclosure_cleared` boolean: 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` string
    - `secrecy_frame_cleared` boolean: 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` string: 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` boolean: The recommended action was lowered. `action_capped_reason` says why. Absent when no cap applied.
    - `action_capped_reason` string
    - `escalation_capped` boolean: Bullying and unsafe results: an escalation was capped. `escalation_capped_reason` says why. Absent when no cap applied.
    - `escalation_capped_reason` string
    - `number_coincidence_cleared` boolean: 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` string
    - `protective_speech_capped` boolean: 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` string
    - `urgency_kept` boolean: An urgent action a declared setting would have capped to review was kept, because the case is acute.
    - `urgency_kept_reason` string
    - `game_play_capped` boolean: 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` string
    - `passing_remark_capped` boolean: 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` string
    - `single_insult_watched` boolean: 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` string
    - `bullying_report_surfaced` boolean: 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` string
    - `rumour_spread_raised` boolean: 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` string
    - `looks_attack_raised` boolean: 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` string
    - `continued_after_objection_raised` boolean: 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` string
    - `declared_age_overridden` boolean: 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` string
    - `willing_exchange_capped` boolean: 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` string
    - `document_safekeeping_capped` boolean: 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` string
    - `ordinary_discipline_capped` boolean: 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` string
    - `ordinary_day_capped` boolean: 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` string
    - `support_suggested` boolean: Distress-signals result: the analysis suggested a supportive check-in and nothing more. `recommended_action` is `monitor`. Absent otherwise.
    - `third_party_report_surfaced` boolean: 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` string
    - `adult_exchange_capped` boolean: 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` string
    - `shared_surprise_capped` boolean: 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` string
    - `meeting_logistics_cleared` boolean: 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` string
    - `long_conversation_raised` boolean: 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` string
    - `figurative_low_mood_capped` boolean: 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` string
    - `banter_capped` boolean: 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` string
    - `no_child_shown_capped` boolean: 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` string
    - `conversation_distress_surfaced` boolean: 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` string
    - `logistics_only_capped` boolean: 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` string
    - `credits_used` number
    - `profanity` object, nullable: 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.
      - `detected` boolean
      - `matches` string[]
    - `risk_terms` object, nullable: 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.
      - `detected` boolean
      - `matches` object[]
        - `term` string
        - `category` 'drug' | 'violence'
  - `summary` object
    - `total_endpoints` number
    - `detected_count` number
    - `highest_risk` object: 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.
      - `endpoint` string
      - `risk_score` number
      - `close_calls` object[]: 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.
        - `endpoint` string
        - `risk_score` number
    - `overall_risk_level` string
    - `processing_time_ms` number
    - `total_credits_used` number
    - `vulnerability_modifier_applied` boolean
    - `analysis_status` 'complete' | 'partial': `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` string: 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` string: Present only when `analysis_status` is `partial`. What could not be analysed, and what to do about it.
    - `failed_endpoints` object[]: Detectors that could not produce an analysis after one retry (only present if any failed)
      - `endpoint` string: The endpoint that failed
      - `code` string: The error code, as on an error response
      - `error` string: A public description of the failure
    - `skipped_endpoints` string[]: Endpoints skipped because they are disabled in account detection settings
    - `skipped_reason` string: Reason why endpoints were skipped
  - `cross_endpoint_modifier` number
  - `support` object: Helpline and support resources relevant to the detected risk categories and country.
  - `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

---

[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)
