> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Report apparent CSAM to the NCMEC CyberTipline

> The detect, preserve, report runbook for apparent child sexual abuse material in media: whether 18 U.S.C. 2258A applies to you, how to read the requirements endpoint, assemble and preserve a CyberTipline report, and hold the evidence for the 90-day window.

# Report apparent CSAM to the NCMEC CyberTipline

When your platform encounters apparent child sexual abuse material (CSAM) in
media, detection alone does not close the legal loop. US providers must report
it to the National Center for Missing & Exploited Children (NCMEC) CyberTipline
and preserve the report contents for 90 days. This runbook walks the full loop
on Orbit: where the detection signal comes from, how to read the reporting
obligation, how to assemble and preserve a filing-ready report with the two
`/api/v1/messages/media/csam-report` endpoints, and which half of the work
belongs to you.

<Warning>
  This page is operator guidance, not legal advice. Whether and how
  18 U.S.C. §2258A applies to your organization is a question for your counsel.
</Warning>

## When the obligation applies to you

18 U.S.C. §2258A applies to providers of an electronic communication service
or remote computing service to the public in the United States. If your
organization uses Orbit to send or receive SMS, MMS, WhatsApp, RCS, or email
media for US users, you are in that position, and the duty fires when your
service encounters **apparent** CSAM. "Apparent" is the statutory standard: a
credible automated or human detection signal triggers the reporting duty; you
do not need certainty, and you do not investigate or adjudicate the material
yourself.

Orbit already produces the detection signals you need:

* The vision-agent image pre-screen classifies uploaded and attached imagery
  and withholds anything it flags as unsafe, including the `csam` category.
* The inbound media threat scan quarantines dangerous inbound MMS and
  attachment bytes before they reach an inbox or the message store (see
  [Inbound media threat scan and quarantine model](/concepts/inbound-media-threat-scan-model)).

Neither surface reports on its own. The endpoints in this runbook take their
signal and close the loop: detect, preserve, report.

## Where the detection signal comes from

The reporting endpoint recognizes four detection sources. Read them from the
requirements endpoint (`detection_sources`) rather than hard-coding them.

| Source | Meaning |
| - | - |
| `image_moderation_shield` | The vision-agent image pre-screen flagged the `csam` category on uploaded or attached media. |
| `media_threat_scan` | The inbound MMS/media threat scan quarantined the media and an analyst escalated it. |
| `analyst_review` | A human trust-and-safety reviewer confirmed the material. A human confirmation always files, regardless of any numeric confidence. |
| `external_classifier` | A hash-match or perceptual-hash (for example PhotoDNA) verdict you wired in, the way an external AV verdict feeds the media threat scan. |

Automated signals (`image_moderation_shield`, `media_threat_scan`,
`external_classifier`) file only at or above the minimum confidence gate,
`min_confidence` (0.5 by default, returned by the requirements endpoint). A
signal below the gate is refused with a 422 so a borderline classifier verdict
is never filed as a statutory report on its own. If a human reviewer confirms
the material, resubmit with `source: "analyst_review"` and it files at any
confidence.

Media ingested before these scanning surfaces were live carries no automated
signal. For that older media, run your normal human review and file with
`source: "analyst_review"`.

## Read the reporting requirement

`GET /api/v1/messages/media/csam-report/requirements` returns the obligation
exactly as the platform models it: the statute, the preservation window, the
incident types, the report lifecycle states, the detection sources, the
required report elements, and the confidence gate. Read it at integration
time and when your compliance program is audited; it is the contract your
reporting pipeline codes against.

<CodeGroup>
  ```bash cURL theme={null}
  curl -s "https://api.orbit.devotel.io/api/v1/messages/media/csam-report/requirements" \
    -H "X-API-Key: $ORBIT_API_KEY"
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/messages/media/csam-report/requirements",
    { headers: { "X-API-Key": process.env.ORBIT_API_KEY } },
  );
  const { data } = await res.json();
  console.log(data.statute, data.preservation_days, data.min_confidence);
  ```
</CodeGroup>

The `data` object of the response:

```json theme={null}
{
  "organization": "National Center for Missing & Exploited Children (NCMEC)",
  "program": "CyberTipline",
  "statute": "18 U.S.C. 2258A",
  "preservation_days": 90,
  "incident_types": [
    "apparent_csam",
    "child_sexual_exploitation",
    "child_sex_trafficking",
    "obscene_material_involving_minor"
  ],
  "report_statuses": ["assembled", "queued", "submitted", "acknowledged", "failed"],
  "detection_sources": [
    "image_moderation_shield",
    "media_threat_scan",
    "analyst_review",
    "external_classifier"
  ],
  "required_elements": [
    "involved_subscriber_or_transmission_information",
    "historical_reference_timestamps",
    "geographic_location_information",
    "the_apparent_csam_and_its_hashes",
    "the_complete_communication_context"
  ],
  "min_confidence": 0.5
}
```

The `required_elements` list is what §2258A(b) contemplates in operator
language: who was involved and how they connected, when the material was
observed, where the sender appeared to be, the material itself identified by
hash, and the surrounding communication. The sections below show where each
element lands in the request.

## Assemble and file a report

`POST /api/v1/messages/media/csam-report` assembles the filing-ready report
and preserves its summary for the statutory window in one call. The body has
three parts:

* `incident` (required): the channel, direction, the message or conversation
  the material arrived on, when it was detected, and the `detection` block:
  `source`, `apparent_csam`, `confidence` in \[0, 1], and the `detector` that
  produced the verdict.
* `media` (required, 1 to 50 items): each item carries at least one hash
  (`md5`, `sha1`, `sha256`, or `photodna`), plus filename, content type, size,
  a `storage_ref` pointing at the preserved bytes (never the bytes
  themselves), and when it was observed.
* `subscriber` (optional): the involved party's contact reference, handle
  (MSISDN, email, or social handle), IP address, coarse location, and the
  timestamps the material was observed. Include what you lawfully hold; these
  are the §2258A(b)(1),(3) elements.
* `reporter` (optional): your ESP name, NCMEC-assigned ESP id, and contact
  details. Omitted fields fall back to your organization reference until you
  set your NCMEC-registered identity.

This worked example files a report from a vision-agent pre-screen verdict at
0.91 confidence. The hashes below are the MD5/SHA-1/SHA-256 of the string
`The quick brown fox jumps over the lazy dog`; substitute the hashes of the
actual preserved file.

<CodeGroup>
  ```bash cURL theme={null}
  curl -s -X POST "https://api.orbit.devotel.io/api/v1/messages/media/csam-report" \
    -H "X-API-Key: $ORBIT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "incident": {
        "type": "apparent_csam",
        "channel": "mms",
        "direction": "inbound",
        "message_id": "msg_5d41c2f0e9b8",
        "detected_at": "2026-10-16T09:12:44Z",
        "detection": {
          "source": "image_moderation_shield",
          "apparent_csam": true,
          "confidence": 0.91,
          "detector": "image-moderation-shield",
          "categories": ["csam"]
        }
      },
      "subscriber": {
        "contact_id": "ctc_8c41d2aa90f3",
        "handle": "+15550137",
        "ip_address": "198.51.100.23",
        "geo_location": "Columbus, OH, US",
        "observed_timestamps": ["2026-10-16T09:12:44Z"]
      },
      "media": [
        {
          "filename": "inbound-1016.jpg",
          "content_type": "image/jpeg",
          "byte_size": 48213,
          "hashes": {
            "md5": "9e107d9d372bb6826bd81d3542a419d6",
            "sha1": "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
            "sha256": "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
          },
          "storage_ref": "quarantine/mms/2026-10-16/inbound-1016.jpg",
          "observed_at": "2026-10-16T09:12:44Z"
        }
      ]
    }'
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/messages/media/csam-report",
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.ORBIT_API_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        incident: {
          type: "apparent_csam",
          channel: "mms",
          direction: "inbound",
          message_id: "msg_5d41c2f0e9b8",
          detected_at: "2026-10-16T09:12:44Z",
          detection: {
            source: "image_moderation_shield",
            apparent_csam: true,
            confidence: 0.91,
            detector: "image-moderation-shield",
            categories: ["csam"],
          },
        },
        subscriber: {
          contact_id: "ctc_8c41d2aa90f3",
          handle: "+15550137",
          ip_address: "198.51.100.23",
          geo_location: "Columbus, OH, US",
          observed_timestamps: ["2026-10-16T09:12:44Z"],
        },
        media: [
          {
            filename: "inbound-1016.jpg",
            content_type: "image/jpeg",
            byte_size: 48213,
            hashes: {
              md5: "9e107d9d372bb6826bd81d3542a419d6",
              sha1: "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
              sha256:
                "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592",
            },
            storage_ref: "quarantine/mms/2026-10-16/inbound-1016.jpg",
            observed_at: "2026-10-16T09:12:44Z",
          },
        ],
      }),
    },
  );
  console.log(res.status, await res.json());
  ```
</CodeGroup>

On success the endpoint returns `201` with the assembled report in `data`.
The report starts life `assembled`: built, preserved, and awaiting
transmission. `preservation.preserve_until` is the §2258A(h) deadline, 90
days after assembly. The `data` object:

```json theme={null}
{
  "reportId": "ctr_4f8a91b2e6d04c3aa7",
  "status": "assembled",
  "legalBasis": "18 U.S.C. 2258A",
  "organization": "National Center for Missing & Exploited Children (NCMEC)",
  "program": "CyberTipline",
  "generatedAt": "2026-10-16T09:15:02.000Z",
  "incident": {
    "type": "apparent_csam",
    "channel": "mms",
    "direction": "inbound",
    "messageId": "msg_5d41c2f0e9b8",
    "detectedAt": "2026-10-16T09:12:44.000Z",
    "detection": {
      "source": "image_moderation_shield",
      "apparentCsam": true,
      "confidence": 0.91,
      "detector": "image-moderation-shield",
      "categories": ["csam"]
    }
  },
  "subscriber": {
    "contactId": "ctc_8c41d2aa90f3",
    "handle": "+15550137",
    "ipAddress": "198.51.100.23",
    "geoLocation": "Columbus, OH, US",
    "observedTimestamps": ["2026-10-16T09:12:44.000Z"]
  },
  "media": [
    {
      "filename": "inbound-1016.jpg",
      "contentType": "image/jpeg",
      "byteSize": 48213,
      "hashes": {
        "md5": "9e107d9d372bb6826bd81d3542a419d6",
        "sha1": "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12",
        "sha256": "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
      },
      "storageRef": "quarantine/mms/2026-10-16/inbound-1016.jpg",
      "observedAt": "2026-10-16T09:12:44.000Z"
    }
  ],
  "preservation": {
    "required": true,
    "statute": "18 U.S.C. 2258A",
    "days": 90,
    "preservedFrom": "2026-10-16T09:15:02.000Z",
    "preserveUntil": "2027-01-14T09:15:02.000Z"
  }
}
```

When the signal does not warrant a statutory report, the endpoint returns
`422` and preserves nothing:

```json theme={null}
{
  "error": {
    "code": "CSAM_REPORT_NOT_WARRANTED",
    "message": "Detection does not warrant a CyberTipline report (not apparent CSAM, or confidence below the reporting threshold)",
    "status": 422
  }
}
```

A 422 is the conservative path doing its job: an over-report has its own
legal cost. It is not an error in your integration.

## What Orbit does and what you do

The split is sharp, and it is the part of this runbook to get right before an
audit.

**Orbit** assembles the filing-ready artifact: it validates the detection
signal against the confidence gate, normalizes the report to the CyberTipline
shape, computes the 90-day preservation deadline, and preserves a PII-safe
summary of the report (references and media hashes, not the subscriber's
handle, IP, or location) in your tamper-evident audit ledger. Orbit does not
transmit anything to NCMEC on your behalf.

**You** are the reporting ESP. Registration with NCMEC, the CyberTipline
credentials, and the transmission step that moves a report from `assembled`
through `queued`, `submitted`, and `acknowledged` are tenant-owned controls.

<Warning>
  Tenant-owned: register your organization with NCMEC as a reporting ESP and
  provision the transmission adapter that submits your assembled reports
  (NCMEC CyberTipline, missingkids.org/cybertipline). Until those credentials
  exist, reports are assembled and preserved for you, but nothing leaves the
  platform. Your counsel and your NCMEC onboarding define the transmission
  cadence; Orbit holds the artifact and the preservation record.
</Warning>

## Evidence preservation and the 90-day window

§2258A(h) requires you to preserve the report contents and the involved
material for 90 days. Every filed report writes a tamper-evident audit entry
(action `messages.csam_report.filed`, resource `csam_cybertip_report`, keyed
by the `ctr_…` report id) carrying the incident classification, the detection
source and confidence, the media hashes, and `preserve_until`. Read it where
you read the rest of the ledger: **Settings → Audit log** in the dashboard,
or the async export covered in [Audit Log Export](/guides/audit-log-export);
the live filters are in [Audit Log](/guides/audit-log).

Two operational consequences:

* The preserved entry and the media it references outrank routine retention
  purges for the statutory window. If your retention schedule or an erasure
  request would delete the quarantined bytes or the audit entry before
  `preserve_until`, the statutory preservation obligation wins for that
  window; coordinate erasure work with your pending reports.
* The audit summary deliberately holds references, not the subscriber PII
  the filing carries. The full report is reconstructable from the referenced
  message at filing time, so keep the underlying message and media readable
  until `preserve_until` as well.

## Troubleshooting

**422 `CSAM_REPORT_NOT_WARRANTED`.** Either `apparent_csam` was `false` or the
automated signal's confidence was below `min_confidence` (0.5 by default;
read the live value from the requirements endpoint). If a human reviewer
confirms the material, resubmit the same body with
`detection.source: "analyst_review"`; human confirmations file at any
confidence.

**400 on the media hashes.** Every `media` item needs at least one of `md5`,
`sha1`, `sha256`, or `photodna`, and the hex hashes must be their full length
(32, 40, and 64 hex characters respectively). Hash the preserved bytes, not a
thumbnail or a transcode.

**401 or 403.** The GET requires your read scope and the POST your write
scope on Messages. Check the API key used by your reporting pipeline, not the
key your dashboard session uses.

**No detection signal on older media.** Media ingested before the scanning
surfaces were live has no automated verdict. File those after human review
with `source: "analyst_review"`, and see
[Tenant migration operations](/guides/tenant-migration-ops) if your
organization still has a platform migration pending.

## See also

* [The media attach pipeline: upload, scan, and per-channel delivery](/concepts/media-attach-pipeline-model)
* [Inbound media threat scan and quarantine model](/concepts/inbound-media-threat-scan-model)
* [Outbound URL reputation and the smishing scan](/concepts/url-reputation-smishing-scan)
* [Audit Log](/guides/audit-log)
* [Audit Log Export](/guides/audit-log-export)
* [Media library cookbook](/guides/media-library-cookbook)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.