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

# Feedback and investigations

> Report outcomes, correct a case, inspect an exact decision, and export an account.

An assessment records what NoResponse knew at one time. An account holds the current decision. An outcome records what your team later learned. An enforcement records what your platform did.

Keep these records separate. Blocking a user is an action, not proof of abuse.

## Report an outcome

Send an account report with your `assess` key and a stable `Idempotency-Key`:

```http Report account abuse theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
POST /v1/accounts/acct_example/outcomes
Idempotency-Key: case_example_report_0001
Content-Type: application/json

{"outcome":"confirmed_abuse","external_ref":"case_example"}
```

The response is a receipt that does not change. It records the report id, observation time, receipt time and actor. It also names the effective report and the account versions before and after the write.

A retry with the same key and body returns that receipt. Reusing the key with different content returns `idempotency_key_reused`.

An abuse report adds one account-level reason in this environment. It does not change earlier assessments or affect another workspace's accounts. A flow decision change sends `account.risk_changed`.

Apply the event with the highest account `risk_version`. Use changed-account polling to recover missed deliveries.

The existing `POST /v1/assessments/{assessment_id}/outcomes` operation keeps its behavior. Its identifier marks are separate from account feedback. Correct a report through the operation that recorded it.

## Correct or retract a report

Get `GET /v1/accounts/{account_id}/outcomes` with a `history` key. The page's `effective_outcome_id` names the current report. Each receipt keeps its original effective report and risk version.

A correction names the current report in `supersedes`:

```json Correct a report theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "outcome": "false_positive",
  "supersedes": "out_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
  "external_ref": "appeal_example"
}
```

Use a new idempotency key. A stale correction returns `outcome_conflict` rather than replacing a newer report. `legitimate` and `false_positive` clear the account feedback reason. Other evidence can still require review or blocking.

`unknown` alone records uncertainty without changing the current report. `unknown` with `supersedes` retracts that report.

Reports expire under the environment's retention. Maintenance clears an expired report's reason and advances the account version. An account read also resolves an expired reason before returning the current decision. Historical decisions keep the reason that applied at their version.

## Find accounts and assessments by a submitted identifier

Select an email, phone number or username in an account or assessment to open
the corresponding search. The console keeps the workspace, environment, match
mode and return path. Results come from bounded server queries.

The two search routes accept the same `identifier` object:

```json Identifier filter theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "identifier": {
    "kind": "email",
    "value": "Jane.Doe+case@EXAMPLE.COM",
    "match": "exact"
  },
  "limit": 25
}
```

Use `POST /v1/accounts/search` for distinct accounts. An account matches when any
retained admitted assessment submitted the identifier, even when its latest
assessment sent a different value. Use `POST /v1/assessments/search` for the
individual assessments that sent it. Add the required `from` and `to` range.
Its decisions are assessment decisions, separate from current account risk.

### Combine submitted identifiers

Use `identifier_expression` to combine email, phone and customer username terms
with `and` and `or`. Each term uses the match rules below.
Choose the scope explicitly:

| `scope` | What must match | Search route |
| - | - | - |
| `assessment` | One retained assessment must satisfy the whole expression. | Accounts or assessments |
| `account` | Different retained assessments of one account can satisfy different terms. | Accounts only |

Suppose an account submitted an email yesterday and a phone today.
An AND search with `scope: "account"` can find it.
The same search with `scope: "assessment"` requires both identifiers in one submission.
Neither mode combines identifiers from different accounts.

This example finds one submission with the phone and either email:

```json Phone with either submitted email theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "identifier_expression": {
    "scope": "assessment",
    "expression": {
      "and": [
        { "term": { "kind": "phone", "value": "+14155552671", "match": "canonical" } },
        {
          "or": [
            { "term": { "kind": "email", "value": "case@example.com", "match": "exact" } },
            { "term": { "kind": "email", "value": "other@example.com", "match": "exact" } }
          ]
        }
      ]
    }
  },
  "limit": 25
}
```

Send it to `POST /v1/accounts/search` for distinct accounts.
For assessment results, use `POST /v1/assessments/search` and add `from` and `to`.
That range applies to the returned submissions.
Change the scope to `account` to search across an account's retained history.
Account scope has no time filter. Effective retention still limits each term's
matching submissions. Assessment search rejects account scope with `400 invalid_request`.

In the console, **Submitted together** and **Across account history** express these scopes.
Assessment filters use **Submitted together**. Account filters offer both choices.
AND requires every child. OR requires at least one child.

#### Expression limits and duplicate terms

Each node contains exactly one member: `term`, `and` or `or`.
A term contains the existing `identifier` shape.
A group has 2 to 8 children. The expression allows at most three nested group
levels and eight term occurrences in total. A single root term is allowed.

Repeated terms count toward the eight-term limit.
Terms with the same kind, match rule, namespace and normalized comparison have
one effect and one matching record. Different match rules remain distinct.
Duplicates do not require separate submissions. Empty groups, unknown members,
unsupported scopes and expressions beyond these limits return `400 invalid_request`.

Choose one filter: `identifier_expression`, `identifier` or legacy `email`.
`POST /v1/accounts/search` also takes `email_domain`, as a fourth choice. It finds the accounts that sent an email of the domain or its subdomains, as an `email_domain` list entry matches them.
Other filters combine with the chosen filter using AND.
The current permissions and submitted-only boundary apply to every term.
Source discoveries cannot complete a combination.

#### Read the matching submissions

Expression results include `matched_identifiers` instead of `matched_identifier`.
Each entry preserves the original submitted value and its `assessment_id`.
It also carries `kind`, `match_basis`, `provenance: "submitted"` and the username
`namespace` when needed.

`term_index` identifies a term's first position in the request, starting at zero.
Count leaves in depth-first order, visiting children in their listed order.
The example above numbers the phone 0 and the emails 1 and 2.
The response lists each satisfied unique term in that order.
It includes satisfied terms from OR branches even when another branch already
satisfied the expression.

With assessment scope, every matching record refers to one submission.
Account results choose the newest submission that satisfies the whole expression.
With account scope, each term uses its newest retained matching submission.
These assessment IDs can differ. The account's `latest` and `current` values
keep their separate meanings.

Deleting or expiring one term's only matching submission can exclude an account
from an AND search. Another retained matching submission can preserve it.
Account deletion excludes all its matches.

#### Continue a combined search

Expression searches keep the existing ordering, count limits and live-page rules
below. Accounts appear once, even when several submissions or branches match.
The count describes distinct accounts on account search and assessments on
assessment search.

A cursor binds the scope, ordered expression, original term values and other
filters. Keep operand order, grouping, values and match rules unchanged.
Omitting `match` means `exact` and does not change that binding.
Equivalent spelling or Boolean rearrangement still changes the filter.
Changing only `limit` is allowed.

```ts TypeScript theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { NoResponse, type IdentifierExpression } from "@noresponse/node";

const nr = new NoResponse({ apiKey: process.env.NORESPONSE_API_KEY! });
const identifierExpression: IdentifierExpression = {
  scope: "account",
  expression: {
    and: [
      { term: { kind: "email", value: "case@example.com", match: "exact" } },
      { term: { kind: "phone", value: "+14155552671", match: "canonical" } },
    ],
  },
};
const page = await nr.accounts.search({ identifierExpression, limit: 25 });
// Use these witnesses in your authorized investigation UI.
export const matchingSubmissions = page.items.flatMap(item =>
  (item.matchedIdentifiers ?? []).map(match => ({
    accountId: item.accountId,
    termIndex: match.termIndex,
    submittedValue: match.value,
    assessmentId: match.assessmentId,
  })),
);
```

```python Python theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import os
from noresponse import IdentifierExpression, NoResponse

expression = IdentifierExpression.model_validate({
    "scope": "account",
    "expression": {"and": [
        {"term": {"kind": "email", "value": "case@example.com", "match": "exact"}},
        {"term": {"kind": "phone", "value": "+14155552671", "match": "canonical"}},
    ]},
})
with NoResponse(api_key=os.environ["NORESPONSE_API_KEY"]) as nr:
    response = nr.accounts.search(identifier_expression=expression, limit=25)
    assert response.result is not None
    # Use these witnesses in your authorized investigation UI.
    matching_submissions = [
        {
            "account_id": item.account_id,
            "term_index": match.term_index,
            "submitted_value": match.value,
            "assessment_id": match.assessment_id,
        }
        for item in response.result.items
        for match in item.matched_identifiers or []
    ]
```

All seven SDKs expose `IdentifierExpression` and `IdentifierTermMatch` models.
Python supports the same filter on synchronous and asynchronous search methods.
The MCP tools accept the wire shape under `request.identifier_expression`.

### Choose the match rule

`exact` is the default. It compares the original submitted text, including case
and formatting. NoResponse preserves the original value for every mode.

| Kind | `canonical` | Additional mode |
| - | - | - |
| Email | NFC-normalise the local part while preserving its case. Lowercase the domain. Keep dots, tags and the original domain. | `mailbox` explicitly selects list matching: lowercase the address and ignore `+tag`. At Gmail, ignore local-part dots and map `googlemail.com` to `gmail.com`. |
| Phone | Use the explicit international E.164 form. Ignore accepted separators. NoResponse does not infer a country. | No mailbox mode. NoResponse refuses extensions and invalid numbers. |
| Username | NFC, Unicode case fold, then NFC, in the customer's submitted username namespace. | Requires `namespace: "customer"`. Source-platform usernames are not included. |

For example, canonical email matches `Jane.Doe+case@EXAMPLE.COM` with
`Jane.Doe+case@example.com`, while retaining the capitalised local part and tag.
It does not match `jane.doe@example.com`. Choose `mailbox` when you intentionally
want the existing alias rules. Those rules are a matching policy, not proof of
who controls either address.

Email and username search values follow the syntax accepted for submitted
identifiers, including Unicode local parts and usernames. Email is bounded to
254 characters overall and 64 in its local part. Usernames are 1 to 64
characters. Controls, invisible format characters and whitespace are refused.
Phone queries allow up to 64 characters so a canonical query can include
formatting. An accepted submission itself is strict E.164. The canonical phone
rule ignores spaces, hyphens, periods and parentheses, and the optional domestic
`(0)` after an explicit country code. Country codes `39`, `378` and `379` keep
that zero. This is syntax matching, not verification that a number is assigned
or controlled by the account.

Explicit `mailbox` mode keeps the legacy list validator's byte bounds after
NFC and lowercasing. The local part can contain up to 64 UTF-8 bytes. The full
address can contain up to 254 bytes. Applying alias rules must leave a nonempty
local part.
An admitted Unicode address can therefore be valid for `exact` and `canonical`
but return `400 invalid_request` for `mailbox`. Use the first two modes for its
submitted-value searches. Selecting mailbox mode does not relax these bounds.

A username query is explicit about its scope:

```json Customer username filter theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "identifier": {
    "kind": "username",
    "value": "janedoe",
    "match": "canonical",
    "namespace": "customer"
  }
}
```

NoResponse does not equate this name with a handle on GitHub, Instagram or
another platform. A source-discovered association remains source evidence.
Search does not turn it into a submitted identifier, verified ownership claim
or account merge. The search contract has no discovered-provenance selector.

### Read why an item matched

A single-identifier search adds `matched_identifier` to each item. It contains the original
`value`, `kind`, `match_basis`, `provenance: "submitted"`, matching
`assessment_id`, and the username `namespace` when applicable. For an account,
it identifies the most recent retained assessment that matched. The account's
`latest` assessment may contain a different identifier.

The account row's `current` contains its current account decision and score.
When `current` is absent, show **No current decision**. The latest assessment
score does not fill that gap. The match describes deterministic equality and
does not assign an ownership confidence score.

### Limits, permissions and page changes

Search is limited to the authenticated workspace and environment. It requires
history or investigation access. Filtering by an identifier also requires
permission to read identifiers. API keys need legacy `history`, or `investigate`
with `evidence`/`detailed`. Console members need the corresponding current
capabilities. Detailed assessment responses have their own
evidence permission. Revoked access is checked again on every request.

Use one of `identifier_expression`, `identifier` or the legacy `email` field. All other supported
filters combine with AND. Account search also supports exact `account_id` and `needs_action`, combined
with the identifier using AND. `needs_action` additionally requires review
access for console members or `management` for API keys and returns
`waiting_for` reasons. Work through those accounts with [Reviews](https://docs.noresponse.org/reference/reviews.md). Assessment search keeps its existing time, decision,
revision, evidence and score filters and their combination rules below.

Limits are 1 to 50 items, default 25 accounts or 20 assessments. Every page
includes `total` and `total_capped`. The count is exact below 10,000. At or above
10,000 the count is capped and `total_capped` is true. Unsupported identifier
kinds, namespaces, modes and invalid values return `400 invalid_request`.
Missing authentication returns 401. Insufficient current access returns 403.

Pass `next_cursor` with unchanged filters. A cursor is opaque, expires, and is
bound to its operation, workspace, environment and filters. A modified,
expired or cross-scope cursor returns `400 invalid_cursor`. Single and combined
identifier account searches use stable opaque account order, so repeated assessments do not move an existing account
across the cursor. Assessment results use creation time with an ID tie break.
The original directory and legacy email searches keep their existing order.

Items and totals share one database snapshot within a page. Pages are live and
do not share a snapshot. A new match before the cursor needs a fresh search.
Deletion, erasure, expiry or changed filter membership can exclude later results.
Current account risk is read separately and can advance during a page. Start a
new search to refresh the entire result set.

Historical submitted values stay searchable only while their originating
assessment is retained. Deleting or erasing that assessment excludes its match.
Deleting an account excludes all of its matches. Effective retention applies at
read time as well as cleanup. Values from before original inputs were retained
cannot be reconstructed.

For older retained assessments with available original inputs, the existing
maintenance worker and search requests catch up the index in bounded batches.
While catch-up is incomplete, the API returns
`503 service_unavailable` with `Retry-After` instead of a partial page or total.
Retry the same query after that interval. A successful page covers all eligible
retained originals in that page's database snapshot. Historical assessments
whose originals were not retained are outside this guarantee.

### Call the public SDK

> [tab] TypeScript
>
> ```ts Search by submitted email theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> import { NoResponse, type IdentifierSearch } from "@noresponse/node";
>
> const nr = new NoResponse({ apiKey: process.env.NORESPONSE_API_KEY! });
> const identifier: IdentifierSearch = {
>   kind: "email",
>   value: "Jane.Doe+case@example.com",
>   match: "canonical",
> };
> const accounts = await nr.accounts.search({ identifier, limit: 25 });
> for (const item of accounts.items) {
>   console.log(item.accountId, item.current?.risk.decision ?? "No current decision");
>   console.log(item.matchedIdentifier?.value, item.matchedIdentifier?.assessmentId);
> }
> const assessments = await nr.assessments.search({
>   from: "2026-10-01T00:00:00Z",
>   to: "2026-10-02T00:00:00Z",
>   identifier,
>   limit: 20,
> });
> console.log(assessments.total);
> ```

> [tab] Python
>
> ```python Search by submitted username theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> import os
> from noresponse import IdentifierSearch, NoResponse
>
> identifier = IdentifierSearch(
>     kind="username", value="janedoe", match="canonical", namespace="customer"
> )
> with NoResponse(api_key=os.environ["NORESPONSE_API_KEY"]) as nr:
>     account_response = nr.accounts.search(identifier=identifier, limit=25)
>     accounts = account_response.result
>     assert accounts is not None
>     for item in accounts.items:
>         decision = item.current.risk.decision if item.current else "No current decision"
>         matched = item.matched_identifier
>         if matched is not None:
>             print(item.account_id, decision, matched.value)
>     assessments = nr.assessments.search(
>         from_="2026-10-01T00:00:00Z",
>         to="2026-10-02T00:00:00Z",
>         identifier=identifier,
>     )
>     print(assessments.total)
> ```

Python also provides `search_async` on both resources. Account search keeps its
existing operation envelope: read the page from `.result`, with headers on the response. Assessment search keeps its existing direct-page
return. The generated Go, Java,
C#, PHP and Ruby clients use the same `IdentifierSearch` and `IdentifierMatch`
models. The MCP server exposes `search_accounts` and `search_assessments`, with
the filter under `request.identifier`. The API checks the caller's permissions
for every tool call.

### List linked accounts

`GET /v1/accounts/{account_id}/links` lists the other accounts in the
same workspace and environment that submitted an email, phone or username that
this account also submitted. It runs the account's own retained submitted
identifiers through the same match rules as search, so you do not search each
identifier by hand. The account itself is not listed.

```json Linked account page theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "schema_version": "1",
  "account_id": "acct_7f3a9c",
  "items": [
    {
      "account_id": "acct_91b2d4",
      "shared_identifiers": [
        {
          "kind": "email",
          "match_basis": "mailbox",
          "provenance": "submitted",
          "account": {
            "value": "jane.doe@gmail.com",
            "assessment_id": "asm_01JA2Z8K6Q4V7W9XBCDEFGHJKM",
            "submitted_at": "2026-10-14T09:00:00.412Z"
          },
          "linked_account": {
            "value": "janedoe+2@gmail.com",
            "assessment_id": "asm_01JA3B1C2D3E4F5G6H7J8K9M0N",
            "submitted_at": "2026-10-20T17:42:10.006Z"
          }
        }
      ]
    }
  ],
  "total": 1,
  "total_capped": false
}
```

Each linked account has one `shared_identifiers` entry for each shared kind, in
the order email, phone, username. The entry uses the strictest rule that links
the two accounts: `exact`, then `canonical`, then `mailbox`. `account` and
`linked_account` each name that account's most recent retained assessment that
submitted the identifier, with the original value and `submitted_at`. Under
`canonical` or `mailbox` the two values can differ. A shared identifier is
evidence that both accounts submitted it, not proof that one person owns both.

Only submitted identifiers link accounts. Identifiers that a source found do not.
An expired or deleted assessment links nothing, on either side. An
account with no retained assessments returns an empty page.

Pass `limit` (1 to 50, default 25) and `cursor` as query parameters. Accounts
come in a stable order. `total` counts distinct linked accounts, exactly below
10,000. The cursor is bound to the account, workspace and environment. A changed
or expired cursor returns `400 invalid_cursor`. The permissions are those of
identifier search: an API key needs `history`, or `investigate` with `evidence`,
and a console member needs history and evidence access in that environment.
While older assessments are still being indexed the API returns
`503 service_unavailable` with `Retry-After`.

> [tab] TypeScript
>
> ```ts List linked accounts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> import { NoResponse } from "@noresponse/node";
>
> const nr = new NoResponse({ apiKey: process.env.NORESPONSE_API_KEY! });
> const page = await nr.accounts.listLinked({ accountId: "acct_7f3a9c", limit: 25 });
> for (const linked of page.items) {
>   for (const shared of linked.sharedIdentifiers) {
>     console.log(linked.accountId, shared.kind, shared.matchBasis, shared.linkedAccount.value);
>   }
> }
> ```

> [tab] Python
>
> ```python List linked accounts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
> import os
> from noresponse import NoResponse
>
> with NoResponse(api_key=os.environ["NORESPONSE_API_KEY"]) as nr:
>     page = nr.accounts.list_linked("acct_7f3a9c", limit=25)
>     for linked in page.items:
>         for shared in linked.shared_identifiers:
>             print(linked.account_id, shared.kind, shared.match_basis, shared.linked_account.value)
> ```

Python also provides `list_linked_async`. The MCP server exposes
`list_linked_accounts`, with `account_id`, `limit` and `cursor` under `request`.
In the console, the account page and the assessment page list linked accounts
and link to each one.

## Search and get exact decisions

`POST /v1/assessments/search` searches the calling key's environment. Supply an inclusive `from` and exclusive `to`, no more than 366 days apart. Optional filters are account, flow, status, assessment recommendation and assessment decision. With `"revision": "first"`, the filters apply to each assessment's first answer and search returns that answer. Use it to find the assessments behind a count from [Assessment counts](https://docs.noresponse.org/reference/aggregates.md).

You can also filter by `band`, `reason`, `impact`, incomplete `lane`, `country`, `asn`, `score_min`, `score_max` and `list_match`. Filters combine with AND. `impact` requires a reason, and score bounds are inclusive. Pending answers without a score do not match score filters.

`list_match` accepts `allow`, `block` or `none` and requires `revision: first`. `none` means NoResponse captured that no list entry matched. Older answers without captured list state do not match any of these values.

The legacy `email` filter keeps its account-wide mailbox behavior. Search finds each account that sent that mailbox in one of its retained assessments. It returns every assessment of those accounts, also the assessments that sent another address. The mailbox matches as a list entry does. Case does not matter and NoResponse ignores a `+tag`. At `gmail.com` and `googlemail.com`, it also ignores the dots of the local part. So `Jane.Doe+promo@googlemail.com` finds `janedoe@gmail.com`. An address without a mailbox answers 400. A search by email needs the permission that reads identifiers. For an API key, that is `history`, or `detailed` with `investigate` or `exports`. A workspace member needs the evidence permission.

Pass `changed: true` to find the assessments whose latest decision differs from their first answer's, such as an `allow` that a late result turned into `block`. `changed: false` finds the others. An assessment whose first answer had no decision yet is not changed. `changed` cannot be combined with `revision: "first"`.

Pages have at most 50 items. Pass `next_cursor` back with the same filters. Each page has `total`, the number of assessments that match the filters across every page, and `total_capped`. `total` is exact up to 10,000. At 10,000 or more matches it is 10,000 and `total_capped` is `true`.

```json Search assessments theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  "from": "2026-01-01T00:00:00Z",
  "to": "2026-01-02T00:00:00Z",
  "flow": "account_creation",
  "recommendation": "review",
  "limit": 50
}
```

Search returns the latest retained version of each matching assessment. Results can change between pages as work settles, and so can `total`. The existing account-only history query keeps its behavior.

Each item also has `first_decision`, the decision of the assessment's first answer, so you can show what changed without another read. It is absent when the first answer had no decision yet.

Use `decision: "block"` to find blocked assessments. The decision is `block` when the stored assessment has `risk.block: true`. Otherwise, it is the recommendation. Later account feedback or shared-key changes do not change this filter.

`recommendation` and `decision` combine with AND. Neither filter means the account's current decision.

Get an exact assessment with `GET /v1/assessments/{assessment_id}/versions/{version}`. It includes the original policy, flow, depth and decision. It excludes the current account projection.

Get an exact account decision with `GET /v1/accounts/{account_id}/decisions/{risk_version}`. It identifies the original assessment version and the feedback report that applied. Its risk includes the shared-key state that applied then. A version from before capture began returns 404, not a reconstruction from current data.

Both operations need `investigate` or `history`. A detailed assessment view also needs `evidence` or `detailed`. Submitted identifiers require that sensitive evidence access too.

## Diagnose an HTTP attempt

`GET /v1/requests` needs `history`. Supply `from` and `to` for a range of at most seven days. Optional filters are `request_id`, normalized `route`, and HTTP `status`. Pages have at most 100 records.

Each attempt has a separate id, even when retries share an `X-Request-ID`. Assessment attempts include the exact public decision prepared for that response. `write_complete` means the server wrote and flushed the response. It does not prove your platform processed it.

Diagnostics exclude request bodies, authorization values and source payloads. NoResponse keeps them for seven days or the shorter environment retention. Capture has a bounded time budget to protect response latency. Infrastructure failures can leave gaps.

Private metrics count failed diagnostic writes. Check this count and query coverage before using diagnostics for evaluation. An incomplete collection cannot establish complete operational coverage.

Routes use templates without path values. Credential operations use `issued_keys` in diagnostic templates to preserve the label through security filtering. Their HTTP paths remain `/credentials`.

## Bind a provisional account

A hosted `pre_registration` check can return a provisional id: `pre:` followed by the assessment id. After your auth provider creates the user, bind the assessment to the stable opaque account id:

```http Bind the account theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
POST /v1/assessments/asm_01JA2Z8K6Q4V7W9XBCDEFGHJKM/bind
Content-Type: application/json

{"account_id":"acct_example"}
```

The operation needs `assess`. It creates a new assessment version and keeps earlier versions. Pending work continues on that assessment and updates the stable account. Repeating the same binding returns the original receipt.

A different target returns `binding_conflict`. Bind before adding outcomes, enforcements or issued keys to the provisional account. This operation does not merge arbitrary accounts.

Keep the binding receipt and use the stable id for later actions. While the binding exists, deleting either name deletes the bound account. The binding expires with its assessment. Use the stable id for later data requests.

## Associate keys issued by your platform

`POST /v1/accounts/{account_id}/credentials` accepts `{"sha256":"<64 lowercase hex characters>"}` and needs `assess`. Compute the hash from a high-entropy key issued by your platform. Send the hash, never the key.

The operation returns a new association id or the original record for a repeated hash. A hash linked to another account returns a conflict.

`GET /v1/accounts/{account_id}/credentials` lists associations and needs `history`. `DELETE /v1/accounts/{account_id}/credentials/{credential_id}` retires an association and needs `delete`. Retirement keeps its first actor and time. It is safe to repeat.

Retirement excludes the association from risk calculations. Revoke the actual key through your platform. Repeating registration after retirement keeps the association retired.

Usage batches for active associations use the existing shared-key rules. Historical decisions keep their original shared-key state. Associations remain while retained account activity needs them. Retired records prevent old assessment links from activating the association again.

Deleting the account deletes these associations immediately.

## Export an account

`GET /v1/accounts/{account_id}/export` needs `exports` and `investigate`, or the legacy `history` scope. It returns one consistent snapshot of retained records. Detailed assessment fields also need `evidence` or `detailed`.

Each assessment revision in the export carries `detail.identifiers`. The export includes assessments and revisions, captured account decisions, outcomes, binding receipts, key associations, enforcements and request diagnostics.

The response names its exclusions. These include expired or deleted data, secrets, key hashes, restricted source records and other workspaces' data. It also excludes legacy free-text notes and historical decisions from before capture began. `complete` describes the retained records within those boundaries.

A synchronous export supports at most 10,000 records and 8 MiB. Larger accounts return `export_too_large`, never a truncated success.

For a larger account, create an asynchronous export with `POST /v1/workspaces/{workspace_id}/exports`. Choose `kind: account`, the account id and the environment. Poll the returned job, then download its numbered chunks before expiry. Each download checks your current permissions.

An asynchronous job permits up to 64 MiB. Artifacts expire within 24 hours, or sooner when their source retention ends. Use paged history and search for larger collections. Account exports exclude investigator reviews. [Export a review](https://docs.noresponse.org/reference/reviews.md#export-a-review) separately with review and export access.

For the job lifecycle, chunk downloads, cancellation, retries and limits, see [Export jobs](https://docs.noresponse.org/reference/data-retention.md#export-jobs).

## Evaluate a threshold change

Keep the first served decision, the outcome learned later, and every correction. Use the decision from before the first outcome observation or receipt. Include the cutoff from a report that a correction later replaced.

Count each canonical account once. Several assessments or a provisional id must not create extra samples.

Unknown and unlabeled users are not known legitimate users. Your reports are assertions, so document how your team collected the labels. Set aside accounts for a holdout before choosing thresholds.

Compare false positives, abuse detection, review volume, latency, unavailable results, service errors and missing diagnostics. Include unlabeled accounts when measuring review volume. Synthetic outcomes test integration behavior, not production accuracy.

Apply reviewed changes through the existing settings operation. A change affects new assessments. It does not rewrite historical decisions.


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