Skip to main content
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.
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 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. 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. A key without the scope answers 403 workspace_forbidden. A workspace member needs one of three actions in the environment: 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

string
required
The group’s name, at most 200 characters.
string
required
When the occasion that the group is for starts, as an RFC 3339 time.
string
When it ends, at or after starts_at. The default is starts_at.
string
The place that the group comes to, at most 300 characters.
string
The organisation that receives the group, at most 200 characters.
object[]
The people the group protects, at most 20. Each has a name and up to 10 aliases.
string[]
Sensitive topics that the screening compares posts with, at most 50.
The answer is 201 with the group and a Location header:
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.

List, read and change groups

GET /v1/workspaces/{workspace_id}/groups/{environment} answers the newest groups first:
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:
Each change adds 1 to version. A different expected_version answers 409 group_conflict. Read the group again and decide whether your change still applies.

Delete a group

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.
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.
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. A missing or malformed key answers 400 invalid_idempotency_key. A group holds at most 20000 people. A batch that goes past the bound answers 409 group_limit_reached and adds nobody.

Start a screening

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. 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:
One screening of a group runs at a time. A second start while one is queued or running answers 409 group_conflict. A retry with the same key and body answers the same screening, with Idempotent-Replayed: true. The start answers 409 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 and queues nothing. 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.

Follow a screening

You do not have to poll. When the screening finishes, NoResponse sends screening.completed to your verified webhook endpoints, and 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:
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.

Cancel a screening

Send an empty JSON object:
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

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:

Read a person’s report

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

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. The reasons are these: A platform’s status is one of these: 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 to your verified webhook endpoints, without the note.
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:
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: 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:
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:
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. 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:
The answer is 202 with the search:
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:
A search that is still counting answers 409 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. A field of another kind answers 400 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. 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 shows how to read them, and 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, 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 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:
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:

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.