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

# Groups and screenings

> Put a group and its people into an environment, screen the people, read each person's report and record your team's decision, under /v1/workspaces/{workspace_id}/groups/{environment}.

A group is one list of people that your security team screens together. Examples are the guests of a conference, the contractors of a site and the visitors of a building. You give NoResponse the group, the principals it protects and its sensitive topics. Then you add the people and start a screening. The screening runs for minutes or hours, away from the three second assessment path. When it finishes, you read the roster, open the report of each person and mark each person `cleared`, `watch` or `escalated`.

> [!NOTE]
>
> The test environment screens with the synthetic connector. It contacts no platform. It makes up profiles, posts and findings from each person's details, and the same list always gets the same results. A live screening answers 409 [`screening_unavailable`](https://docs.noresponse.org/problems/screening-unavailable) until NoResponse turns on a collector for each platform that you ask for.

## Routes and access

Every route is under `/v1/workspaces/{workspace_id}/groups/{environment}`. `environment` is `live` or `test`.

| Route | What it does |
| - | - |
| `GET /groups/{environment}` | Pages through the groups, newest first. |
| `POST /groups/{environment}` | Creates a group. |
| `GET /groups/{environment}/{group_id}` | Reads a group and its person count. |
| `PATCH /groups/{environment}/{group_id}` | Changes the group's details. |
| `DELETE /groups/{environment}/{group_id}` | Deletes the group with its people, decisions and screenings. |
| `GET /groups/{environment}/{group_id}/people` | Pages through the roster, filtered by band and decision. |
| `POST /groups/{environment}/{group_id}/people` | Adds or updates up to 500 people. |
| `GET /groups/{environment}/{group_id}/people/{person_id}` | Reads the report of one person. |
| `POST /groups/{environment}/{group_id}/people/{person_id}/review` | Records a decision on a person. |
| `POST /groups/{environment}/{group_id}/people/{person_id}/records` | Joins one of the person's records to them, or rejects it. |
| `POST /groups/{environment}/{group_id}/people/{person_id}/searches` | Starts a new search on a value of one of the person's records. |
| `POST /groups/{environment}/{group_id}/people/{person_id}/searches/{search_id}/run` | Runs a counted password or hash search. |
| `GET /groups/{environment}/{group_id}/screenings` | Pages through the screenings, newest first. |
| `POST /groups/{environment}/{group_id}/screenings` | Starts a screening. |
| `GET /groups/{environment}/{group_id}/screenings/{screening_id}` | Reads a screening's status and progress. |
| `POST /groups/{environment}/{group_id}/screenings/{screening_id}/cancel` | Stops a queued or running screening. |

An API key needs the `groups` scope for every route. A key acts only in its own workspace and environment. The other environment answers 404 [`not_found`](https://docs.noresponse.org/problems/not-found). A key without the scope answers 403 [`workspace_forbidden`](https://docs.noresponse.org/problems/workspace-forbidden).

A workspace member needs one of three actions in the environment:

| Action | Routes | Roles |
| - | - | - |
| `groups.read` | Every `GET` route. | Owner, admin, developer, investigator, viewer and auditor. |
| `groups.write` | Create, change and delete a group, add people, and start and cancel a screening. | Owner and admin. A developer in the test environment only. |
| `groups.review` | Record a decision on a person, join or reject one of its records, and search again on a value of one of its records. | Owner, admin and investigator. |

[Roles](https://docs.noresponse.org/reference/team-and-audit.md#roles) describes each role.

Ids have a prefix: `grp_` for a group, `per_` for a person, `scn_` for a screening, `arv_` for a decision and `srh_` for a search.

## Create a group

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Annual shareholder meeting", "starts_at": "2026-12-03T09:00:00Z", "ends_at": "2026-12-03T17:00:00Z", "venue": "Harbour Hall, Sydney", "host": "Example Holdings", "principals": [{"name": "Jordan Lee", "aliases": ["J. Lee"]}], "topics": ["executive pay", "site closures"]}'
```

**name** `string` *required*

The group's name, at most 200 characters.

**starts\_at** `string` *required*

When the occasion that the group is for starts, as an RFC 3339 time.

**ends\_at** `string`

When it ends, at or after `starts_at`. The default is `starts_at`.

**venue** `string`

The place that the group comes to, at most 300 characters.

**host** `string`

The organisation that receives the group, at most 200 characters.

**principals** `object[]`

The people the group protects, at most 20. Each has a `name` and up to 10 `aliases`.

**topics** `string[]`

Sensitive topics that the screening compares posts with, at most 50.

The answer is 201 with the group and a `Location` header:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "environment": "test",
  "name": "Annual shareholder meeting",
  "starts_at": "2026-12-03T09:00:00Z",
  "ends_at": "2026-12-03T17:00:00Z",
  "venue": "Harbour Hall, Sydney",
  "host": "Example Holdings",
  "principals": [{"name": "Jordan Lee", "aliases": ["J. Lee"]}],
  "topics": ["executive pay", "site closures"],
  "people": 0,
  "version": 1,
  "purge_after": "2027-01-02T17:00:00Z",
  "created_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
  "created_at": "2026-11-08T08:00:00Z",
  "updated_at": "2026-11-08T08:00:00Z"
}
```

`purge_after` is 30 days after the group ends. At that time NoResponse purges the raw items that screenings collected and the candidate profiles that matched no person. `created_by` is the key, or the member, that created the group. An environment holds at most 1000 groups. One more answers 409 [`group_limit_reached`](https://docs.noresponse.org/problems/group-limit-reached).

## List, read and change groups

`GET /v1/workspaces/{workspace_id}/groups/{environment}` answers the newest groups first:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test?limit=20" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY"
```

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "items": [
    {
      "id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
      "environment": "test",
      "name": "Annual shareholder meeting",
      "starts_at": "2026-12-03T09:00:00Z",
      "ends_at": "2026-12-03T17:00:00Z",
      "venue": "Harbour Hall, Sydney",
      "host": "Example Holdings",
      "principals": [{"name": "Jordan Lee", "aliases": ["J. Lee"]}],
      "topics": ["executive pay", "site closures"],
      "people": 2,
      "version": 1,
      "purge_after": "2027-01-02T17:00:00Z",
      "created_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
      "created_at": "2026-11-08T08:00:00Z",
      "updated_at": "2026-11-08T08:00:00Z"
    }
  ],
  "next_before": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM"
}
```

`limit` is 1 to 100, and the default is 20. When `next_before` is present, pass it back as `before` to read the next page. An unknown or repeated query parameter answers 400.

`GET /v1/workspaces/{workspace_id}/groups/{environment}/{group_id}` answers one group, in the same form as the create answer.

`PATCH` changes only the fields that you send. Send `expected_version` to change the group only if nobody changed it since you read it:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X PATCH "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"venue": "Harbour Hall, Level 2", "expected_version": 1}'
```

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "environment": "test",
  "name": "Annual shareholder meeting",
  "starts_at": "2026-12-03T09:00:00Z",
  "ends_at": "2026-12-03T17:00:00Z",
  "venue": "Harbour Hall, Level 2",
  "host": "Example Holdings",
  "principals": [{"name": "Jordan Lee", "aliases": ["J. Lee"]}],
  "topics": ["executive pay", "site closures"],
  "people": 2,
  "version": 2,
  "purge_after": "2027-01-02T17:00:00Z",
  "created_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
  "created_at": "2026-11-08T08:00:00Z",
  "updated_at": "2026-11-08T08:05:00Z"
}
```

Each change adds 1 to `version`. A different `expected_version` answers 409 [`group_conflict`](https://docs.noresponse.org/problems/group-conflict). Read the group again and decide whether your change still applies.

## Delete a group

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X DELETE "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY"
```

The answer is 204 with no body. The delete takes the group with its people, their decisions and its screenings, and stops a screening that still runs. After the delete, the group answers 404.

## Add people

Send 1 to 500 people in one request, with an `Idempotency-Key` header. The body is at most 512 KiB.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: roster-2026-11-08-1" \
  -d '{"people": [{"external_id": "reg-1042", "name": "Sam Rivera", "email": "sam.rivera@example.com", "company": "Example Fund", "handles": [{"platform": "x", "handle": "samrivera"}], "role": "guest"}, {"external_id": "reg-1043", "name": "Alex Chen", "phone": "+61412345678", "city": "Melbourne"}]}'
```

Each row needs a `name`. The other fields help the screening find the person. `external_id` is your own id for the person, unique within the group. Then come `email`, `phone` in E.164 form, `company`, `city` and up to 20 known `handles`. `role` is the person's role in the group: `guest` (a ticket holder), `crew`, `vendor` or `press`. Leave it out when you do not know it. A person without a role has no role, not `guest`. A handle names its `platform`: `instagram`, `x`, `bluesky`, `youtube`, `tiktok`, `facebook`, `threads`, `reddit`, `telegram`, `linkedin`, `github`, `scratch`, `dailymotion`, `substack`, `duolingo`, `dockerhub`, `wikipedia`, `internetarchive`, `spotify`, `patreon`, `wordpress`, `okru`, `flickr` or `synthetic`. A screening of `github` or of `scratch` through `flickr` reads the public profile the handle names, with no posts.

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "created": 2,
  "updated": 0,
  "items": [
    {"index": 0, "id": "per_01JA2Z9A1B2C3D4E5F6G7H8J9K", "outcome": "created"},
    {"index": 1, "id": "per_01JA2Z9A1B2C3D4E5F6G7H8J9M", "outcome": "created"}
  ]
}
```

Each item gives the position of its row, the person's id and the outcome:

* `created`: a new person.
* `updated`: the row names an existing person of the group and changes its details. A row names a person by the same `external_id`. A row without `external_id` names a person by the same `email`, and then by the same `phone`. The row replaces the person's details. The person goes back to `unscreened`, because its earlier results describe other details.
* `unchanged`: the row names an existing person with the same details.

A retry with the same key and body within 24 hours answers the first answer again, with the header `Idempotent-Replayed: true`. The same key with a different body answers 409 [`idempotency_key_reused`](https://docs.noresponse.org/problems/idempotency-key-reused). A missing or malformed key answers 400 [`invalid_idempotency_key`](https://docs.noresponse.org/problems/invalid-idempotency-key). A group holds at most 20000 people. A batch that goes past the bound answers 409 [`group_limit_reached`](https://docs.noresponse.org/problems/group-limit-reached) and adds nobody.

## Start a screening

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/screenings" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: screening-2026-11-08-1" \
  -d '{"depth": "standard", "platforms": ["instagram", "x"]}'
```

`depth` is `standard`, which reads the bio and the last 100 posts of each profile. `platforms` names 1 to 11 platforms from the list above. `synthetic` is the synthetic connector's own platform. In the test environment, the synthetic connector also serves `instagram`, `x`, `bluesky` and `youtube`.

In the live environment, a screening reads `bluesky` through Bluesky's public API, from a `bluesky` handle that you supply. It reads `youtube`, once NoResponse turns it on, through the YouTube Data API, from a `youtube` handle, channel link or channel id that you supply. A screening does not search YouTube by name. When a person has no handle for a platform, the screening has nothing to look up. It lists that platform in the coverage field `not_searched`, not in `platforms`. That platform is not `none_found` and not a failure, and the person is not partial. When no platform that you asked for has anything to look up, the person has no profile to check. See [Match states](#match-states). Both read public profiles and posts only, and never sign in.

The screening covers each person that no screening has reached, and each person on `watch` or `escalated`. The answer is 202 with the screening, a `Location` header and `Retry-After: 5`:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
  "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "environment": "test",
  "status": "queued",
  "depth": "standard",
  "platforms": ["instagram", "x"],
  "requested_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
  "created_at": "2026-11-08T08:10:00Z",
  "expires_at": "2026-11-09T08:10:00Z",
  "people_total": 2,
  "people_done": 0,
  "people_flagged": 0,
  "people_failed": 0,
  "people_partial": 0,
  "estimated_spend": 120,
  "spent": 0,
  "spend": {"total_minor": 0, "by_implementation": []}
}
```

One screening of a group runs at a time. A second start while one is `queued` or `running` answers 409 [`group_conflict`](https://docs.noresponse.org/problems/group-conflict). A retry with the same key and body answers the same screening, with `Idempotent-Replayed: true`. The start answers 409 [`screening_unavailable`](https://docs.noresponse.org/problems/screening-unavailable) when a platform that you ask for has no collector turned on in the environment. In the test environment, `tiktok`, `facebook`, `threads`, `reddit`, `telegram`, `linkedin`, `github` and the profile sites `scratch` through `flickr` have none yet.

### Spend and the daily cap

`estimated_spend` is the cost estimate of the screening in US cents: the people that it selects, at its depth and on its platforms. `spent` is the cost that its screened people recorded so far. A failed person records the cost of its lookups only, because the screening keeps them. `spend` breaks the cost of the screening down by implementation and role. It also counts the other costs of people that failed and of passes that were retried, so its `total_minor` can be more than `spent`. The `screening_daily_cap` setting of the environment limits the spend of all its screenings in one UTC day. The default is 50000, which is 500 US dollars. When the estimate of a new screening is more than the cap has left, the start answers 409 [`screening_cap_exceeded`](https://docs.noresponse.org/problems/screening-cap-exceeded) and queues nothing. [Cap screening spend](https://docs.noresponse.org/reference/settings.md#cap-screening-spend) tells you how the API calculates the remainder and how to change the cap.

Each person that a screening screens is one `screenings` unit in your [usage](https://docs.noresponse.org/reference/team-and-audit.md#usage).

## Follow a screening

You do not have to poll. When the screening finishes, NoResponse sends [`screening.completed`](https://docs.noresponse.org/guides/webhooks#screening-completed) to your verified webhook endpoints, and [`person.flagged`](https://docs.noresponse.org/guides/webhooks#person-flagged) for each person that it flags. The member who started the screening from the console also gets an email with the counts.

To follow it yourself, poll the screening at its `Location`:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/screenings/scn_01JA2ZA0B1C2D3E4F5G6H7J8K9" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY"
```

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
  "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "environment": "test",
  "status": "completed",
  "depth": "standard",
  "platforms": ["instagram", "x"],
  "requested_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
  "created_at": "2026-11-08T08:10:00Z",
  "started_at": "2026-11-08T08:10:01Z",
  "finished_at": "2026-11-08T08:10:03Z",
  "expires_at": "2026-11-09T08:10:00Z",
  "people_total": 2,
  "people_done": 2,
  "people_flagged": 1,
  "people_failed": 0,
  "people_partial": 0,
  "estimated_spend": 120,
  "spent": 14,
  "spend": {"total_minor": 14, "by_implementation": [{"implementation": "synthetic", "role": "collector", "spend_minor": 9}, {"implementation": "synthetic", "role": "analyser", "spend_minor": 5}]}
}
```

| `status` | Meaning |
| - | - |
| `queued` | The screening waits for a worker. |
| `running` | The screening is reaching people. `people_done` grows as it goes. |
| `completed` | The screening reached every person on every platform. |
| `partial` | The screening finished. `people_failed` people stay uncovered, and `people_partial` people have a platform that it could not read. |
| `failed` | The screening covered no person. `error_code` says why. |
| `cancelled` | Somebody cancelled the screening. `error_code` is `user_cancelled`. |

`people_flagged` counts the people with a finding. `spend` is what the screening cost so far, in US cents, in total and for each resolver, collector and analyser. A screening waits when its analyser does not answer. It tries again after 30 seconds, then after longer waits, up to 9 tries, as a webhook delivery does. A screening still open at `expires_at`, 24 hours after you started it, finishes `partial` or `failed` with `error_code` `expired`. A failed person keeps its earlier state, and the next screening tries it again. The screening keeps the lookups that it made for that person. The next pass or screening uses them again and does not pay for them again.

`GET .../screenings` answers the group's screenings, newest first, as `items` with `next_before`. It takes `limit` and `before` like the group list.

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "items": [
    {
      "id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
      "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
      "environment": "test",
      "status": "completed",
      "depth": "standard",
      "platforms": ["instagram", "x"],
      "requested_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
      "created_at": "2026-11-08T08:10:00Z",
      "started_at": "2026-11-08T08:10:01Z",
      "finished_at": "2026-11-08T08:10:03Z",
      "expires_at": "2026-11-09T08:10:00Z",
      "people_total": 2,
      "people_done": 2,
      "people_flagged": 1,
      "people_failed": 0,
      "people_partial": 0,
      "estimated_spend": 120,
      "spent": 14,
      "spend": {"total_minor": 14, "by_implementation": [{"implementation": "synthetic", "role": "collector", "spend_minor": 9}, {"implementation": "synthetic", "role": "analyser", "spend_minor": 5}]}
    }
  ]
}
```

## Cancel a screening

Send an empty JSON object:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/screenings/scn_01JA2ZA0B1C2D3E4F5G6H7J8K9/cancel" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
  "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "environment": "test",
  "status": "cancelled",
  "depth": "standard",
  "platforms": ["instagram", "x"],
  "requested_by": "nrk_test_01JA2ZC0Q1R2S3T4V5W6X7Y8Z9",
  "created_at": "2026-11-08T08:10:00Z",
  "started_at": "2026-11-08T08:10:01Z",
  "finished_at": "2026-11-08T08:10:02Z",
  "expires_at": "2026-11-09T08:10:00Z",
  "people_total": 2,
  "people_done": 1,
  "people_flagged": 0,
  "people_failed": 0,
  "people_partial": 0,
  "estimated_spend": 120,
  "spent": 4,
  "spend": {"total_minor": 4, "by_implementation": [{"implementation": "synthetic", "role": "collector", "spend_minor": 2}, {"implementation": "synthetic", "role": "analyser", "spend_minor": 2}]},
  "error_code": "user_cancelled"
}
```

The people that the screening did not reach keep their earlier state. A screening that already finished answers 200 with its final state and does not change.

## Read the roster

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people?decision=undecided&limit=100" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY"
```

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "items": [
    {
      "id": "per_01JA2Z9A1B2C3D4E5F6G7H8J9K",
      "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
      "external_id": "reg-1042",
      "name": "Sam Rivera",
      "email": "sam.rivera@example.com",
      "company": "Example Fund",
      "handles": [{"platform": "x", "handle": "samrivera"}],
      "role": "guest",
      "match_state": "none_found",
      "highest_band": null,
      "decision": null,
      "coverage": {"accounts_found": 0, "accounts_accessible": 0, "items_read": 0, "platforms": [{"platform": "instagram", "status": "none_found", "accounts_found": 0, "items_read": 0}, {"platform": "x", "status": "none_found", "accounts_found": 0, "items_read": 0}]},
      "spend": {"total_minor": 0, "by_implementation": []},
      "last_screening_id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
      "screened_at": "2026-11-08T08:10:02Z",
      "created_at": "2026-11-08T08:01:00Z",
      "updated_at": "2026-11-08T08:10:02Z"
    }
  ],
  "next_after": "per_01JA2Z9A1B2C3D4E5F6G7H8J9K"
}
```

The roster answers people in the order that you added them. `limit` is 1 to 500, and the default is 100. When `next_after` is present, pass it back as `after` to read the next page. Three filters narrow the roster:

* `band` is `critical`, `high`, `moderate`, `low`, `info`, or `none` for people with no finding.
* `decision` is `cleared`, `watch`, `escalated`, or `undecided` for people with no decision.
* `role` is `guest`, `crew`, `vendor`, `press`, or `unstated` for people with no role.

Each person has these screening fields:

| Field | Meaning |
| - | - |
| `match_state` | How sure the latest screening is that it found the person's accounts: `confirmed`, `probable`, `possible`, `none_found` or `inaccessible`. `unscreened` means that no screening has reached the person yet, or that the latest screening had no profile to check. It never reads as `none_found`. |
| `no_profile_to_check` | `true` when the latest screening had nothing to look up on each platform that you asked for, such as no handle. Absent otherwise. See [Match states](#match-states). |
| `highest_band` | The most severe band of the person's findings, or `null` when nothing was flagged. |
| `decision` | Your team's latest decision, or `null`. |
| `coverage` | What the latest screening read: accounts found and readable, items read, the period, and each platform's status. Until a screening reaches the person, it is zero with no platforms. See [Coverage](#coverage). |
| `spend` | What the latest screening of the person cost, in US cents. |
| `last_screening_id`, `screened_at` | The latest screening that reached the person, and when. |

## Read a person's report

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people/per_01JA2Z9A1B2C3D4E5F6G7H8J9M" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY"
```

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "person": {
    "id": "per_01JA2Z9A1B2C3D4E5F6G7H8J9M",
    "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
    "external_id": "reg-1043",
    "name": "Alex Chen",
    "phone": "+61412345678",
    "city": "Melbourne",
    "match_state": "confirmed",
    "highest_band": "moderate",
    "decision": "watch",
    "decision_note": "Ask the venue team to watch the side exit.",
    "decided_by": "usr_security_lead",
    "decided_at": "2026-11-08T08:20:00Z",
    "coverage": {
      "accounts_found": 2,
      "accounts_accessible": 1,
      "items_read": 2,
      "period_from": "2026-02-14T08:30:00Z",
      "period_to": "2026-10-30T19:05:00Z",
      "platforms": [
        {"platform": "instagram", "status": "inaccessible", "accounts_found": 1, "items_read": 0},
        {"platform": "x", "status": "covered", "accounts_found": 1, "items_read": 2}
      ]
    },
    "spend": {"total_minor": 3, "by_implementation": [{"implementation": "synthetic", "role": "collector", "spend_minor": 2}, {"implementation": "synthetic", "role": "analyser", "spend_minor": 1}]},
    "last_screening_id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
    "screened_at": "2026-11-08T08:10:02Z",
    "created_at": "2026-11-08T08:01:00Z",
    "updated_at": "2026-11-08T08:20:00Z"
  },
  "profiles": [
    {
      "id": "prf_01JA2ZB1C2D3E4F5G6H7J8K9M0",
      "platform": "x",
      "handle": "alexchen42",
      "url": "https://synthetic.invalid/x/alexchen42",
      "display_name": "Alex Chen",
      "bio": "Engineer in Melbourne. Views my own.",
      "avatar_url": "https://synthetic.invalid/avatar/3f9a1c22d4e5b6a7.png",
      "followers": 1834,
      "account_created_at": "2019-04-01T00:00:00Z",
      "access": "public",
      "match": {
        "state": "confirmed",
        "evidence": [
          {"kind": "name", "detail": "the profile's display name", "resolver": "synthetic"},
          {"kind": "phone", "detail": "the phone on the person row", "resolver": "synthetic"},
          {"kind": "source_fact", "detail": "an account an assessment source found for that phone", "resolver": "synthetic"}
        ]
      },
      "collector": "synthetic",
      "items_read": 2,
      "period_from": "2026-02-14T08:30:00Z",
      "period_to": "2026-10-30T19:05:00Z",
      "collected_at": "2026-11-08T08:10:02Z"
    },
    {
      "id": "prf_01JA2ZB1C2D3E4F5G6H7J8K9M1",
      "platform": "instagram",
      "handle": "alexchen42",
      "url": "https://synthetic.invalid/instagram/alexchen42",
      "display_name": "Alex Chen",
      "access": "private",
      "match": {"state": "probable", "evidence": [{"kind": "name", "resolver": "synthetic"}, {"kind": "city", "resolver": "synthetic"}]},
      "collector": "synthetic",
      "items_read": 0,
      "collected_at": "2026-11-08T08:10:02Z"
    }
  ],
  "findings": [
    {
      "id": "fnd_01JA2ZB2C3D4E5F6G7H8J9K0M1",
      "category": "movement_exposure",
      "band": "moderate",
      "confidence": 0.8,
      "rationale": "The post reports when and where the principal leaves the hotel.",
      "quote": "Spotted Jordan Lee leaving the hotel, they always take the side exit at seven.",
      "evidence": ["itm_01JA2ZB3C4D5E6F7G8H9J0K1M2"],
      "analyser": "synthetic",
      "model": "synthetic-1",
      "spend_minor": 1,
      "created_at": "2026-11-08T08:10:02Z"
    }
  ],
  "posts": [
    {
      "id": "itm_01JA2ZB3C4D5E6F7G8H9J0K1M2",
      "kind": "post",
      "profile_id": "prf_01JA2ZB1C2D3E4F5G6H7J8K9M0",
      "permalink": "https://synthetic.invalid/x/alexchen42/p0",
      "posted_at": "2026-10-30T19:05:00Z",
      "text": "Spotted Jordan Lee leaving the hotel, they always take the side exit at seven.",
      "screenshot_sha256": "9c56cc51b374c3ba189210d5b6d4bf57790d351c96c47c02190ecf1e430635ab",
      "captured_at": "2026-11-08T08:10:02Z"
    }
  ],
  "media": [],
  "reviews": [
    {
      "id": "arv_01JA2ZB0C1D2E3F4G5H6J7K8M9",
      "decision": "watch",
      "note": "Ask the venue team to watch the side exit.",
      "created_by": "usr_security_lead",
      "created_at": "2026-11-08T08:20:00Z"
    }
  ]
}
```

The report comes from the latest screening that reached the person:

* `profiles` lists the accounts that the screening matched, with the evidence for each match.
* `findings` lists what the screening flagged, the most severe first. Each finding cites its evidence by item id, and `quote` gives the exact words of the first item that it rests on. A finding on a picture has `media_observations`, one for each picture: `description` tells what the picture shows, and `read_text` gives the words that the analyser read in it. These words come from the picture, not from the text of a post. Thus they are not a quote, and NoResponse cannot check them. Compare them with the picture at its `permalink`.
* `posts` and `media` are the items that the findings cite, with the permalink, the time and a SHA-256 hash of the screenshot.
* `reviews` lists every decision on the person, oldest first.

### Match states

A screening gives each profile a match state from its evidence. Email, phone, a handle that you supplied and an account that an assessment source found are hard identifiers. Company, city and school are soft identifiers.

| Match state | Evidence |
| - | - |
| `confirmed` | A handle that you supplied for the person, or two or more different hard identifiers. |
| `probable` | One identifier beyond the name. |
| `possible` | The name only. |

The person's `match_state` is the strongest match among the profiles that the screening could read. When it found only private or locked accounts, the state is `inaccessible`. When it looked on every platform and found no account, the state is `none_found`.

When the person gave the screening nothing to look up on each platform that you asked for, nobody looked for a profile. An example is a row with no handle on a platform that a screening reads from handles only, such as Bluesky in live. The person then has `no_profile_to_check: true`. Its `match_state` stays `unscreened`, `highest_band` is `null`, and `coverage.not_searched` names each platform. This is a final result. It is not `none_found` and not a failure, and it does not make the screening partial. The person's lookups still run, show on its report and count in the spend. Screen again does not select the person. To screen the person, add a handle to its row. The changed row goes back to unscreened.

### Bands and categories

A finding has one band: `critical`, `high`, `moderate`, `low` or `info`, the most severe first. The analyser raises a finding one band when its evidence names the group or one of its days. The words that it read in a picture count as evidence. It does the same for a principal, an alias, the host or the venue. The match of its evidence then caps the band. A finding on a `possible` match is at most `moderate`. A finding on a `probable` match is at most `high`. When the cap lowers a band, `capped_from` gives the band that the analyser gave.

A finding is about behaviour toward the group's venue, occasion or principals, never about views in general. Its `category` is one of these:

| Category | What the evidence shows |
| - | - |
| `direct_threat` | A threat to the principal, or to the venue or occasion of the group. |
| `harm_intent` | An intent to harm, with no named target. |
| `fixation` | A fixation on the principal. |
| `grievance` | A grievance against the host. |
| `security_research` | Research into the venue, the route or the security. |
| `weapons` | Weapons, tied to one of the other categories. |
| `disruption` | Plans to disrupt or protest at the venue or occasion of the group. |
| `violent_extremism` | Violent extremist content or identification. |
| `harassment` | Earlier harassment or stalking of staff or principals. |
| `movement_exposure` | Posts of the principal's addresses or movements. |

### Coverage

`coverage` tells a clean result apart from a person that the screening could not see. It is empty until a screening reaches the person.

| Field | Meaning |
| - | - |
| `accounts_found` | The accounts that the screening matched. |
| `accounts_accessible` | The matched accounts that it could read. |
| `items_read` | The posts and media that it read. |
| `items_not_analysed` | The items that it read but could not analyse, such as videos. A finding may be missing from them. Absent when it analysed every item. |
| `not_analysed_by_reason` | `items_not_analysed` for each reason, as `reason` and `items`. Absent when it analysed every item. |
| `period_from`, `period_to` | The oldest and the newest item that it read. |
| `platforms` | One entry for each platform that you asked for. |

The reasons are these:

| Reason | Meaning |
| - | - |
| `video` | A video. The screening reads its caption, but not the video. |
| `image_unavailable` | The screening could not get the picture from the platform. |
| `image_unsupported` | The picture is not JPEG, PNG, GIF or WebP, or it is too large. |
| `image_limit` | The person has more pictures than the screening reads. It reads 100 pictures for each person. |
| `refused` | The model did not read the item. |
| `unfinished` | The model stopped before it read the item, or its answer was not complete. |
| `unverified` | A finding cited the item, but NoResponse could not check it. The quote is not in the item, or the finding did not tell what the picture shows. |
| `unspecified` | Your analyser gave no reason. |

A platform's `status` is one of these:

| Status | Meaning |
| - | - |
| `covered` | The screening read at least one account. |
| `none_found` | The screening looked and found no account. |
| `inaccessible` | The screening found accounts, but they are private or locked. |
| `failed` | The screening could not finish its search. It is not `none_found`. |
| `unavailable` | NoResponse turned on no collector for the platform. |

A `failed` or `unavailable` platform makes the person partial, and so do items that the analyser could not analyse. The screening counts the person in `people_partial`.

`spend` is what the latest screening of the person cost, in US cents, in total and for each resolver, collector and analyser.

## Record a decision

Each decision also sends [`person.decided`](https://docs.noresponse.org/guides/webhooks#person-decided) to your verified webhook endpoints, without the note.

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people/per_01JA2Z9A1B2C3D4E5F6G7H8J9K/review" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"decision": "watch", "note": "Ask the venue team to confirm the badge photo."}'
```

`decision` is `cleared`, `watch` or `escalated`. `note` is at most 2000 characters, for the venue team and for later reviewers. The answer is 200 with the person:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "id": "per_01JA2Z9A1B2C3D4E5F6G7H8J9K",
  "group_id": "grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "external_id": "reg-1042",
  "name": "Sam Rivera",
  "email": "sam.rivera@example.com",
  "company": "Example Fund",
  "handles": [{"platform": "x", "handle": "samrivera"}],
  "match_state": "none_found",
  "highest_band": null,
  "decision": "watch",
  "decision_note": "Ask the venue team to confirm the badge photo.",
  "decided_by": "usr_security_lead",
  "decided_at": "2026-11-08T08:20:00Z",
  "coverage": {"accounts_found": 0, "accounts_accessible": 0, "items_read": 0, "platforms": [{"platform": "instagram", "status": "none_found", "accounts_found": 0, "items_read": 0}, {"platform": "x", "status": "none_found", "accounts_found": 0, "items_read": 0}]},
  "spend": {"total_minor": 0, "by_implementation": []},
  "last_screening_id": "scn_01JA2ZA0B1C2D3E4F5G6H7J8K9",
  "screened_at": "2026-11-08T08:10:02Z",
  "created_at": "2026-11-08T08:01:00Z",
  "updated_at": "2026-11-08T08:20:00Z"
}
```

Decisions add up. The latest decision is the person's `decision`, and the report keeps the earlier ones. A person holds at most 1000 decisions. The next screening of the group covers every person on `watch` or `escalated` again.

## Whose record it is

A search on a name or a common username finds many people. So each record in the report's `lookup.records` says how likely it is to be the person:

| Field | Meaning |
| - | - |
| `likelihood` | From 0.01 to 0.99. A record that the person's own email or phone found starts at 0.99. Any other record starts lower when its search found more people on the same site. |
| `agreed` | The names of the record's fields whose values agree with what NoResponse knows of the person. Each one raises `likelihood`. A rare value raises it more than a value that many of the found records hold. The value that found the record is not in the list. |
| `clashed` | The names of the record's fields that differ from the person's birth date or full name. Each one lowers `likelihood`. |
| `joined` | `true` when the record joins the person: its `likelihood` is 0.9 or more, or your team joined it. Otherwise the record is a lead. |
| `verdict` | `joined` or `rejected` when your team overruled the record. |

NoResponse knows the person from two things. One is the person's row. The other is the values of their own records: those that their email or phone found, and those that your team joined. A record that joins only by its `likelihood` adds nothing to what NoResponse knows, so one wrong join does not pull in more. One shared value alone does not take a record to 0.9, unless it is the person's email or phone.

To overrule a record, send its id and your verdict:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people/per_01JA2Z9A1B2C3D4E5F6G7H8J9K/records" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"record_id": "rec_3f9a1c22d4e5b6a73f9a1c22d4e5b6a7", "verdict": "rejected"}'
```

`joined` makes the record the person's at any `likelihood`. `rejected` keeps it a lead at any `likelihood`, and a rejected record that the person's email or phone found then adds nothing to what NoResponse knows. The answer is 200 with the verdict:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "record_id": "rec_3f9a1c22d4e5b6a73f9a1c22d4e5b6a7",
  "verdict": "rejected",
  "decided_by": "usr_security_lead",
  "decided_at": "2026-11-08T08:25:00Z"
}
```

The verdict stays while the lookups find the same record, also in later screenings. A later verdict on the record replaces it. A record that the person's latest lookup does not hold answers 404 [`not_found`](https://docs.noresponse.org/problems/not-found). A member needs `groups.review` in the environment. A key needs the `groups` scope. The audit log records each verdict as `person.record_judged`.

## Search again

A username or a password in one record often leads to the person's other accounts. You can start a new search on the value of any field whose `kind` is `email`, `phone`, `username`, `person_name`, `password`, `hash` or `ip`. The field can be in a lookup record, in a profile's record or in a record that an earlier search found. Nothing searches again by itself: a search starts only when you ask for it.

Each such field carries `distinctive`. Search first the values rated `unique` or `distinctive`: those likely find one person. A `common` value finds many people, and you can still search it.

Name the record and the field. You do not send the value:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people/per_01JA2Z9A1B2C3D4E5F6G7H8J9K/searches" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"record_id": "rec_3f9a1c22d4e5b6a73f9a1c22d4e5b6a7", "field": "username"}'
```

The answer is 202 with the search:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "search_id": "srh_01JA2ZB7Q4V7W9XBCDEFGHJKMN",
  "record_id": "rec_3f9a1c22d4e5b6a73f9a1c22d4e5b6a7",
  "field": "username",
  "kind": "username",
  "value": "jordanexample42",
  "status": "queued",
  "sources": 0,
  "answered": 0,
  "found": 0,
  "estimated_spend": 2,
  "spent": 0,
  "requested_by": "usr_security_lead",
  "created_at": "2026-11-08T08:30:00Z"
}
```

The search runs the breach search and the account and phone lookups that take the value's kind, and finishes seconds later. Read the person to follow it. The report's `searches` list every search, newest first. A search that ran is `completed` when every source it asked answered, `partial` when some did not, and `failed` with an `error_code` when none answered. A failed search does not show whether any source holds the value.

What a search finds joins the person's `lookup.records`. Each found record's `matched` entry names the search in `search`, so you can see the value and the record and field that led to it. The records are leads. NoResponse works out their `likelihood` as for any other record, and you can join or reject them.

### Passwords and hashes

Many people choose the same password. So a `password` or `hash` search counts first. It is `counting`, then `counted`, and `rows` says how many breach rows hold the value. Nothing else runs until you run it:

```bash theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
curl -X POST "https://api.noresponse.org/v1/workspaces/$WORKSPACE_ID/groups/test/grp_01JA2Z8K6Q4V7W9XBCDEFGHJKM/people/per_01JA2Z9A1B2C3D4E5F6G7H8J9K/searches/srh_01JA2ZB7Q4V7W9XBCDEFGHJKM/run" \
  -H "Authorization: Bearer $NORESPONSE_GROUPS_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

A search that is still counting answers 409 [`group_conflict`](https://docs.noresponse.org/problems/group-conflict).

### More rows of one table

A breach search returns at most 20 rows of a table. The lookup's `remaining` names each table with rows left out, and the search that left them out. To get them, search the same field again with that `table`. A search of one table returns up to 100 rows of it.

### Limits

Starting the same search while it is still open answers that search. A record or a field that the person does not hold answers 404 [`not_found`](https://docs.noresponse.org/problems/not-found). A field of another kind answers 400 [`invalid_request`](https://docs.noresponse.org/problems/invalid-request). Each search counts against the environment's `screening_daily_cap`, as a screening does, and one that the cap cannot hold answers 409 [`screening_cap_exceeded`](https://docs.noresponse.org/problems/screening-cap-exceeded). A member needs `groups.review` in the environment. A key needs the `groups` scope. The audit log records `person.search_started` and `person.search_run`.

## How a screening works

A screening runs four steps for each person. The first runs once for the person, and the others run on each platform that you ask for:

**1. Look up**

The signup check's lookups run on the person's email, phone and handles: account presence, the phone lookup, the email domain's registration and the breach search. Their results are context for the reviewer. They do not set a band. The screening keeps the lookups when a later step fails or must try again. The next pass, or the next screening of a person that failed, uses them again and does not pay for them again. A changed row gets new lookups.

**2. Resolve**

Each resolver that NoResponse turns on for the platform looks for the person's accounts. It gives each candidate account with the evidence that ties it to the person. The match state comes from that evidence. Two resolvers are on in every environment. `supplied_handles` makes a candidate of each handle on the person row. `lookup_facts` adds an account that a profile found by the lookups lists. Neither searches the platform. When only they cover a platform and have no account to give, the screening lists the platform in `not_searched`, not as `none_found`.

**3. Collect**

The first collector that answers reads each candidate account: its profile, and its posts and media. A private or locked account is `inaccessible`, not a failure.

**4. Analyse**

The first analyser that answers reads the posts and media of the readable accounts, with the group's details. It gives findings that cite the items. NoResponse caps each finding's band by its match.

NoResponse keeps a catalogue of the resolvers, collectors and analysers. The catalogue says which ones serve each platform in each environment, with their priority and rate. NoResponse changes the catalogue without a release. NoResponse turns on the synthetic connector in the test environment. In the live environment, NoResponse turns on a collector for a platform when that collector is ready.

### Lookups

The report's `lookup` gives what the lookups found. A lookup sends the person's email, phone and up to three distinct handles, as an assessment would send them. The same source controls and your workspace's refused sources apply.

* `checks` has one entry for each lane of each identifier that was looked up. `found` means a source found the identifier, and `not_found` means every source answered and none found it. `partial` means some sources did not answer, and `not_covered` means no source answered or none could be asked. These last two come with a `reason`. A lookup that failed is never `not_found`.
* `summary` has the same type as an assessment's `risk.summary`.
* `details` has the same type as an assessment's `risk.details`. Only a member who can read evidence, or a key with the `detailed` or `evidence` scope, gets it.
* `records` has each account and each breach row that the lookups found, once, with every field that its source gave. Everyone who can read the person gets `records`. [Records](https://docs.noresponse.org/guides/records.md) shows how to read them, and [Whose record it is](#whose-record-it-is) how to overrule one.

The lookups add the `lookups` resolver line to the screening's `spend`. The breach searches of a screening wait for the signup check's searches and leave part of the breach search capacity to assessments. A large list can take longer to screen. The test environment answers the lookups from the [simulator](https://docs.noresponse.org/testing.md), so they contact no source.

### The analysis contract

An analyser reads an analysis request and answers an analysis result. NoResponse can connect an external model through an HTTP form of the same contract:

* NoResponse sends the request as a JSON `POST` body. The headers `NR-Analysis-Id`, `NR-Analysis-Timestamp` and `NR-Analysis-Signature` come with it.
* The signature is `v1=` and the lowercase hex HMAC-SHA256 of the timestamp, a `.` and the raw body, under the analyser's key. It is the [webhook signature](https://docs.noresponse.org/guides/webhooks.md) scheme.
* The analyser answers 200 with the result within 60 seconds.
* NoResponse tries again on a timeout, a 408, a 429 or a 5xx, on the webhook schedule. Any other answer fails the person for that screening.

A request:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "request_id": "anr_01JC8Y2M4N6P8Q0R2S4T6V8W01",
  "person_id": "per_01JC8Y2M4N6P8Q0R2S4T6V8W01",
  "depth": "standard",
  "group": {
    "name": "Annual shareholder meeting",
    "starts_at": "2026-12-01T09:00:00Z",
    "ends_at": "2026-12-01T17:00:00Z",
    "venue": "Example Convention Centre",
    "host": "Example Holdings",
    "principals": [
      {
        "name": "Alex Principal",
        "aliases": [
          "Alex P"
        ]
      }
    ],
    "topics": [
      "executive pay"
    ]
  },
  "profiles": [
    {
      "id": "prf_01JC8Y2M4N6P8Q0R2S4T6V8W01",
      "platform": "synthetic",
      "handle": "alexperson42",
      "url": "https://synthetic.invalid/synthetic/alexperson42",
      "display_name": "Alex Person",
      "bio": "Engineer at Example Corp in Melbourne. Views my own.",
      "avatar_url": "https://synthetic.invalid/avatar/3f9a1c22d4e5b6a7.png",
      "followers": 1834,
      "account_created_at": "2019-04-01T00:00:00Z",
      "access": "public",
      "match": {
        "state": "confirmed",
        "evidence": [
          {
            "kind": "name",
            "detail": "the profile's display name",
            "resolver": "synthetic"
          },
          {
            "kind": "email",
            "detail": "the email on the person row",
            "resolver": "synthetic"
          },
          {
            "kind": "source_fact",
            "detail": "an account an assessment source found for that email",
            "resolver": "synthetic"
          }
        ]
      },
      "collector": "synthetic",
      "items_read": 3,
      "period_from": "2026-02-14T08:30:00Z",
      "period_to": "2026-10-30T19:05:00Z",
      "collected_at": "2026-11-08T10:00:00Z"
    }
  ],
  "posts": [
    {
      "id": "itm_01JC8Y2M4N6P8Q0R2S4T6V8W01",
      "kind": "post",
      "profile_id": "prf_01JC8Y2M4N6P8Q0R2S4T6V8W01",
      "permalink": "https://synthetic.invalid/synthetic/alexperson42/p0",
      "posted_at": "2026-10-30T19:05:00Z",
      "text": "Spotted Alex Principal leaving the hotel, they always take the side exit at seven.",
      "screenshot_sha256": "9c56cc51b374c3ba189210d5b6d4bf57790d351c96c47c02190ecf1e430635ab",
      "captured_at": "2026-11-08T00:00:00Z"
    },
    {
      "id": "itm_01JC8Y2M4N6P8Q0R2S4T6V8W02",
      "kind": "post",
      "profile_id": "prf_01JC8Y2M4N6P8Q0R2S4T6V8W01",
      "permalink": "https://synthetic.invalid/synthetic/alexperson42/p1",
      "posted_at": "2026-02-14T08:30:00Z",
      "text": "Great coffee this morning before the commute."
    }
  ],
  "media": [
    {
      "id": "itm_01JC8Y2M4N6P8Q0R2S4T6V8W03",
      "kind": "media",
      "profile_id": "prf_01JC8Y2M4N6P8Q0R2S4T6V8W01",
      "post_id": "itm_01JC8Y2M4N6P8Q0R2S4T6V8W02",
      "permalink": "https://synthetic.invalid/synthetic/alexperson42/p1",
      "posted_at": "2026-02-14T08:30:00Z",
      "media_type": "image",
      "media_url": "https://synthetic.invalid/media/alexperson42/p1.jpg",
      "text": "A photo"
    }
  ]
}
```

The result cites items of the request by id. An empty `findings` array means that your model flagged nothing in what it read. Put in `not_analysed` the ids of the items that your model could not read, such as videos or a batch that it refused. The screening counts them as not analysed and the person as partial, never as clean. To give the reason for an item, add it to `not_analysed_reasons` as `item` and `reason`. An item with no reason counts as `unspecified`. A finding can carry `quote`, the exact words of its first item. A finding on a picture can carry `media_observations`, with the `media_id` of a picture that it cites, its `description` and its `read_text`:

```json theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "request_id": "anr_01JC8Y2M4N6P8Q0R2S4T6V8W01",
  "findings": [
    {
      "category": "movement_exposure",
      "band": "moderate",
      "confidence": 0.8,
      "rationale": "The post reports where the principal leaves the hotel and when, which exposes their movements before the meeting.",
      "evidence": [
        "itm_01JC8Y2M4N6P8Q0R2S4T6V8W01"
      ],
      "model": "example-model-1"
    },
    {
      "category": "security_research",
      "band": "moderate",
      "confidence": 0.7,
      "rationale": "The picture is a floor plan of the venue with notes on when its loading dock is unguarded.",
      "evidence": [
        "itm_01JC8Y2M4N6P8Q0R2S4T6V8W03"
      ],
      "media_observations": [
        {
          "media_id": "itm_01JC8Y2M4N6P8Q0R2S4T6V8W03",
          "description": "A hand-drawn floor plan of the venue with notes in red.",
          "read_text": "Loading dock unguarded 8:30-8:50"
        }
      ],
      "model": "example-model-1"
    }
  ],
  "not_analysed": [
    "itm_01JC8Y2M4N6P8Q0R2S4T6V8W04"
  ],
  "not_analysed_reasons": [
    {
      "item": "itm_01JC8Y2M4N6P8Q0R2S4T6V8W04",
      "reason": "video"
    }
  ],
  "spend_minor": 2
}
```

## Privacy and the audit log

NoResponse stores each person's details encrypted under your workspace's key. To find the same person in a later batch, it keeps keyed digests of the external id, the email and the phone, never the values. The role does not identify anyone, so it is stored as it is, for the `role` filter. Deleting the group, or the workspace, deletes every person. Deleting the workspace also destroys the key.

NoResponse also stores each profile, post, media item and finding encrypted under your workspace's key. Deleting the group deletes them.

Each change goes into the audit log of the environment, with the member or the key that made it: `group.created`, `group.updated`, `group.deleted`, `group.people_added`, `person.decided`, `screening.started` and `screening.cancelled`. An audit event names ids and the decision, never a person's details.


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