# TrustIn DART API — complete reference

> TrustIn DART is a blockchain AML screening API. Give it a wallet address (KYA) or a transaction hash (KYT) and it traces the funds up to five hops through labeled counterparties, matches every fund-flow path against a ruleset, and returns a 0–100 fund-attribution score with a verdict — accept, review, edd or block — and the exact rules behind it. Every screening is saved as an immutable snapshot. The same API lets a customer manage the rulesets and rules that drive the verdicts.

Machine-readable: OpenAPI 3.1 at https://test-v2.width.live/openapi.json · this file at https://test-v2.width.live/llms-full.txt · index at https://test-v2.width.live/llms.txt · agent skill at https://test-v2.width.live/agent/SKILL.md · MCP at https://test-api.width.live/mcp

## Authentication, base URLs and errors

Authentication: every endpoint takes an API key created in the console under Account › API Keys. Send it as the X-Api-Key header (preferred), as ?apikey=<key> in the query string, or as Authorization: Bearer <JWT> for a console session token. Anonymous calls are rejected with HTTP 401.

Base URLs: production https://api.width.live (backup https://api.trustin.bond); test environment https://test-api.width.live (backup https://test-api.trustin.bond) — a separate database, so ruleset ids differ between production and test. This copy documents the test environment.

Every endpoint is served on two domains: https://test-api.width.live and its backup https://test-api.trustin.bond — the same service, the same data and the same API keys. Use the backup when the primary cannot be reached (the name does not resolve or the connection is refused). Do not resend there a request that got a response or timed out: it may already have run, and a screening would run, and be billed, twice.

Response envelope: every response is JSON {"code": 0, "msg": "success", "data": …}. Errors carry code -1 and a msg that names the problem, and use HTTP status codes: 400 invalid input (the msg names the field), 401 no or invalid key, 403 a builtin ruleset is read-only (clone it first), 404 the id does not exist or is not yours, 409 a rule_code already exists in the ruleset, 429 too many concurrent screenings (back off, or use mode=async).

## The two rulesets

Two rulesets shape a screening. The path-matching ruleset (ruleset_type kya for address screening, kyt_in / kyt_out for the two sides of a transaction) holds rules; each rule fires when a fund-flow path — or, for a rule with max_hops -1, the screened address's own labels — meets all of its conditions, and a hit carries the rule's risk_level and action into the result. The scoring ruleset (ruleset_type scoring) holds a scoring_config: for each hop band and direction a base, per-severity weights, and verdict bands; the score sums, over the distinct inbound edges that carried tainted funds, edge amount ÷ total inflow × base × severity weight; an address whose own labels hit a self-hit rule overrides the sum with the self-hit score (100 by default).

Builtin rulesets — AML KYA Default v1.3, AML KYT-IN Default, AML KYT-OUT Default, the fund-attribution scoring default — are visible to everyone and read-only. Anything a customer creates or clones is theirs to edit. The normal way to customise: clone the builtin, edit the copy, pass the copy's id as ruleset_id (or scoring_ruleset_id) when screening. Screening with ruleset_id 0 uses the builtin default.

## Rules and conditions

Rule fields:
- rule_code — your identifier, unique within the ruleset; names the hit in results and snapshots.
- name, category, description, reference — free text.
- risk_level — low | medium | high | critical (drives the score's severity weight).
- action — alert | review | edd | block | allow (the disposition a hit recommends).
- direction — inbound | outbound | both.
- min_hops / max_hops — path length the rule applies to; max_hops -1 = the address's own labels, no path.
- conditions — array of {parameter, operator, value}; all must hold (see Rule conditions below).
- is_highlighted — highlighted (★) rules are surfaced first in results; presentation only.
- highlight_tags — scopes the ★: non-empty means only hits whose counterparty label (any of its four category levels) contains one of these labels (case-insensitive) are highlighted; empty means every hit of the rule is. Kept independently of is_highlighted (it survives the ★ being switched off).
- sort_order — display order.

Rule conditions — each is {"parameter", "operator", "value"}; a rule fires only when all of its conditions hold.

Condition parameters (label = tested against the counterparty's labels, or the screened address's own in a max_hops -1 rule; path = tested against the fund-flow path):
- primary_category (label, string; == != IN NOT_IN CONTAINS) — Top-level category of a label. Values: One of the primary categories under Label values, e.g. Sanctions, Obfuscation, Cybercrime. Exact match, case-sensitive. Example: {"parameter":"primary_category","operator":"==","value":"Sanctions"}
- secondary_category (label, string; == != IN NOT_IN CONTAINS) — Second-level category of a label, under its primary category. Values: See Label values, e.g. Prohibited Entity, Mixers, Phishing. Exact match, case-sensitive. Example: {"parameter":"secondary_category","operator":"IN","value":["Mixers","Privacy Wallet"]}
- tertiary_category (label, string; == != IN NOT_IN CONTAINS) — Third level: the entity behind the label — for a sanctions label, the list it is on. Values: Entity names as the data source spells them, e.g. Htx, Huionepay, Xinbi, Tornado_cash, US-OFAC, UK-OFSI. Case-insensitive. Example: {"parameter":"tertiary_category","operator":"==","value":"huionepay"}
- quaternary_category (label, string; == != IN NOT_IN CONTAINS) — Fourth level: the address type within the entity. Often empty. Values: e.g. htx-hotwallet, htx-deposit, xinbi-address, us-ofac-sanctions. Case-insensitive. Example: {"parameter":"quaternary_category","operator":"CONTAINS","value":"hotwallet"}
- risk_level (label, string; == != IN NOT_IN CONTAINS) — The risk level the data source gives a label — not the rule's own risk_level field. Values: low, medium, high, critical (lower case). Example: {"parameter":"risk_level","operator":"IN","value":["high","critical"]}
- risk_score (label, number; == != > < >= <=) — The highest label risk_level as a number. Values: critical 90, high 70, medium 50, low 20; 0 when there is no label. Example: {"parameter":"risk_score","operator":">=","value":70}
- hops (path, number; == != > < >= <=) — Path length: 1 is a direct counterparty, 2 one address in between, and so on. Values: 1 to 5. Example: {"parameter":"hops","operator":"<=","value":2}
- inflow_hops (path, number; == != > < >= <=) — Path length, checked on inbound paths only. Values: 1 to 5. Example: {"parameter":"inflow_hops","operator":"<=","value":3}
- outflow_hops (path, number; == != > < >= <=) — Path length, checked on outbound paths only. Values: 1 to 5. Example: {"parameter":"outflow_hops","operator":"<=","value":3}
- amount (path, number; == != > < >= <=) — The path's smallest transfer — the most risk-tainted funds that could have travelled the whole path. A hit reports it as exposureAmount. Values: Amount in USDT/USDC, e.g. 10000. Example: {"parameter":"amount","operator":">","value":10000}
- inflow_total_amount (path, number; == != > < >= <=) — The same figure as amount, checked on inbound paths only. Despite the name it is not a total. Values: Amount in USDT/USDC. Example: {"parameter":"inflow_total_amount","operator":">","value":1000}
- outflow_total_amount (path, number; == != > < >= <=) — The same figure as amount, checked on outbound paths only. Despite the name it is not a total. Values: Amount in USDT/USDC. Example: {"parameter":"outflow_total_amount","operator":">","value":1000}
- risk_rate (path, number; == != > < >= <=) — The path's smallest transfer divided by its last transfer, as a percentage. The last transfer is what the screened address received on that path (inbound) or what the counterparty received (outbound). A hit reports it as riskRatePct. Values: Percent, 0 to 100: 10 means 10%. Example: {"parameter":"risk_rate","operator":">=","value":10}
- direction (path, string; == != IN NOT_IN CONTAINS) — The path's direction. The rule's own direction field does the same and is the usual choice. Values: inbound, outbound. Example: {"parameter":"direction","operator":"==","value":"inbound"}
- chain_name (path, string; == != IN NOT_IN CONTAINS) — The chain the path is on. Values: A chain_name from GET /api/v3/chains written in lower case, e.g. tron, ethereum, bsc. A capitalised value such as Tron never matches. Example: {"parameter":"chain_name","operator":"==","value":"tron"}

Condition operators (aliases are case-insensitive):
- == — Equal; value: String or number. Also accepted: =, eq, equals, is
- != — Not equal; value: String or number. Also accepted: <>, ne, not, not_equals
- IN — Equal to one of the listed values; value: JSON array of strings
- NOT_IN — Equal to none of the listed values; value: JSON array of strings. Also accepted: not in, nin
- CONTAINS — Contains the value as a substring; value: String. Also accepted: like
- > — Greater than; value: Number. Also accepted: gt
- < — Less than; value: Number. Also accepted: lt
- >= — Greater than or equal; value: Number. Also accepted: gte, ge
- <= — Less than or equal; value: Number. Also accepted: lte, le

Label values — categories, primary: secondary. The common categories, not a closed list: the data source adds new ones over time. The labels on real results are the authoritative spelling — subjectTags for the screened address, hits[].pathNodes[].tags along each path.
- Sanctions: Sanctioned Entity, Prohibited Entity, Sanctioned Jurisdiction
- Terrorism Financing: Terrorism Financing
- Other Financial Crimes: Money Laundering, Tax Evasion
- Public Freezing Action: Law Enforcement Freeze/Seizure
- Illicit Markets: Human Trafficking, Illegal Services, Child Sexual Abuse Material (CSAM)
- Cybercrime: Hacker/Thief, Ransomware, Phishing, Ponzi, Pig Butchering, Rug Pull, Honeypot, Fake Presale, Black/Grey Industry
- Obfuscation: Mixers, Privacy Wallet, Privacy Token, Centralized Bridge, Crypto ATM/Kiosk
- Gambling: Online Casino, Sports Betting, Unlicensed Gambling
- High-Risk Entities: CEX in High-Risk Jurisdiction, Sanctioned CEX, CEX with Weak AML/CFT Controls, OTC Desk
- Exchanges & DeFi: CEX, DEX, DeFi Protocol, Lending Protocol, Bridges, NFT Marketplaces
- Other Entities: TradFi, Individual, Merchant, Payments, Escrow
Entity and address-type levels (tertiary_category, quaternary_category) come from the data source as it names things, for example Htx / htx-hotwallet, US-OFAC / us-ofac-sanctions, Blacklisted / blacklisted-address, Xinbi / xinbi-address. They compare case-insensitively.

Condition examples (direction, hop range and conditions of a rule):
- Direct exposure to a sanctioned address: {"direction":"both","min_hops":1,"max_hops":1,"conditions":[{"parameter":"primary_category","operator":"==","value":"Sanctions"}]} — Funds came from or went to a sanctioned address with nothing in between.
- The screened address is itself on the OFAC list: {"direction":"both","min_hops":0,"max_hops":-1,"conditions":[{"parameter":"primary_category","operator":"==","value":"Sanctions"},{"parameter":"tertiary_category","operator":"==","value":"us-ofac"}]} — max_hops -1 tests the screened address's own labels instead of a path; only label conditions count there.
- Mixer funds two hops upstream, at least 10% of what arrived: {"direction":"inbound","min_hops":2,"max_hops":2,"conditions":[{"parameter":"secondary_category","operator":"==","value":"Mixers"},{"parameter":"risk_rate","operator":">=","value":10}]} — Fires on an inbound path whose source is a mixer when the path's smallest transfer is at least 10% of what the screened address received on it.
- Any path to one entity: {"direction":"both","min_hops":1,"max_hops":5,"conditions":[{"parameter":"tertiary_category","operator":"IN","value":["huionepay","xinbi"]}]} — Entity names ignore case, so huionepay matches the label Huionepay.
- Cybercrime proceeds above 1,000 USDT within three hops: {"direction":"inbound","min_hops":1,"max_hops":3,"conditions":[{"parameter":"secondary_category","operator":"IN","value":["Hacker/Thief","Ransomware","Phishing","Pig Butchering"]},{"parameter":"amount","operator":">","value":1000}]} — amount is the path's smallest transfer, so all of the 1,000 could have come from the source.
- Sending to a high-risk counterparty on Tron: {"direction":"outbound","min_hops":1,"max_hops":2,"conditions":[{"parameter":"risk_score","operator":">=","value":70},{"parameter":"chain_name","operator":"==","value":"tron"}]} — risk_score 70 or more means a label of high or critical risk. chain_name is written in lower case.

Condition rules of thumb:
- Every condition of a rule must hold. For "either A or B" list the values with IN, or write two rules.
- Label conditions test the counterparty at the far end of the path — the source on an inbound path, the destination on an outbound one — not the addresses in between. In a rule with max_hops -1 they test the screened address's own labels instead; such a rule ignores path conditions and needs at least one label condition.
- An address can carry several labels, and each label condition passes when any one of them matches, so two conditions may be met by different labels. For the same reason != and NOT_IN pass as soon as one label differs — they do not mean "none of its labels". A counterparty with no labels fails every category and risk_level condition, != and NOT_IN included; its risk_score is 0.
- Case: primary_category, secondary_category, risk_level and direction match exactly, case included; tertiary_category and quaternary_category ignore case; chain_name must be written in lower case.
- An operator that does not fit the parameter is not rejected but never matches: >, <, >=, <= on a text parameter, or IN, NOT_IN, CONTAINS on a number. IN and NOT_IN need a JSON array — a single string never matches.
- inflow_hops and inflow_total_amount are checked only on inbound paths, outflow_hops and outflow_total_amount only on outbound ones; on the other side they are skipped, neither passing nor failing. A rule whose conditions are all skipped does not fire.
- Writes are validated: a condition without a parameter or value, with an operator not listed here, or with a parameter not listed here is rejected with 400, and the message names conditions[i]. An unknown parameter a rule already holds is kept when the rule is updated. The legacy spellings target.tags.*, path.node.tags.*, path.amount, path.risk_amount_usd, path.hops_total and fund_ratio are still accepted.

## Workflows and pitfalls

Common workflows:

1. Screen an address synchronously: POST /api/v3/screen/kya {"chain_name":"Tron","address":"T…"} → data.score.verdict, data.hits, data.exposures.
2. Screen asynchronously (recommended for deep hops or busy addresses): add "mode":"async" → data.job_id; then GET /api/v3/screen/result/{job_id}?type=kya until status is COMPLETE; poll no faster than every 2 seconds. The job computes immediately — polling only reads.
   Every screening is recorded as a snapshot: a sync response carries data.snapshotId, a COMPLETE poll data.snapshot_id. GET /api/v3/snapshots/{snapshot_id} reads it back later — parameters and result.
3. Customise the rules: POST /api/v3/rulesets/clone {"source_ruleset_id": <builtin kya id>, "name": "Ours"} → data.id; POST /api/v3/rulesets/rule/create {"ruleset_id": <id>, "rule_code": "MY_MIXER_EXPOSURE_H2", "name": "…", "risk_level": "high", "action": "review", "direction": "inbound", "min_hops": 2, "max_hops": 2, "conditions": [{"parameter":"primary_category","operator":"==","value":"Obfuscation"},{"parameter":"risk_rate","operator":">=","value":10}]}; then screen with "ruleset_id": <id>.
4. Read before you change: POST /api/v3/rulesets/list {"ruleset_type":"kya"} to find ids; POST /api/v3/rulesets/rule/list {"ruleset_id": <id>} for the catalogue.
5. Highlight what matters: POST /api/v3/rulesets/rule/highlight {"ruleset_id": <id>, "rule_ids": [...], "is_highlighted": true}. To highlight only hits touching one entity, add "highlight_tags": ["Huione"] — omit the field to keep each rule's existing scope, or send [] to clear it back to unscoped.

Pitfalls: builtin rulesets cannot be edited (403) — clone them; ids differ between the test and production environments; a sync screening can take minutes on deep hops — prefer async; a rule needs at least one condition; deleting a ruleset never rewrites past snapshots.

## MCP

MCP: the same API is available to agents as tools over the Model Context Protocol (streamable HTTP) at https://test-api.width.live/mcp (backup https://test-api.trustin.bond/mcp). Connect with the API key as Authorization: Bearer <key> (or the X-Api-Key header). Tools: list_chains, screen_address, screen_transaction, list_transfers, get_screen_result, get_snapshot, list_rulesets, get_ruleset, create_ruleset, update_ruleset, delete_ruleset, clone_ruleset, list_rules, get_rule, create_rule, update_rule, delete_rule, highlight_rules, screen_person, get_person_screening, list_person_screenings. Each tool mirrors the HTTP endpoint of the same name and returns the envelope's data.

## v3 Screening (Chainalysis)

### Supported Chains (v3)

`GET https://test-api.width.live/api/v3/chains`

The chains and canonical assets screening accepts, read live from the investigation backend — a value returned here is one screen/kya and screen/kyt will take. Use it to render a chain selector or validate input instead of hardcoding a list that goes stale when a chain is added. address_family says which address form a chain uses ("evm" or "tron"): a 0x… address is shared by every EVM chain, so resolving a bare address to a chain needs it. default_token is what the screening endpoints use when a request omits token. Auth: API key, same as the other v3 endpoints; nothing is charged — this is metadata, not a screening. GET and POST both work.

**Request parameters:**

_No parameters._

**Example request:**

```bash
curl 'https://test-api.width.live/api/v3/chains' -H 'X-Api-Key: $DART_API_KEY'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "chains": [
      {
        "chain_name": "Ethereum",
        "tokens": [
          "usdc",
          "usdt"
        ],
        "default_token": "usdt",
        "address_family": "evm"
      },
      {
        "chain_name": "Tron",
        "tokens": [
          "usdt"
        ],
        "default_token": "usdt",
        "address_family": "tron"
      },
      {
        "chain_name": "Base",
        "tokens": [
          "usdc"
        ],
        "default_token": "usdc",
        "address_family": "evm"
      }
    ]
  }
}
```

**Response fields (data):**

- `chains[].chain_name` (string) — Value to send as chain_name on the screening endpoints
- `chains[].tokens` (array) — Canonical assets screenable on this chain
- `chains[].default_token` (string) — Asset used when a screening request omits token
- `chains[].address_family` (string) — Address form this chain uses: evm (0x… hex) or tron (base58)


### KYA Screen (v3)

`POST https://test-api.width.live/api/v3/screen/kya`

Chainalysis-aligned address screening. Runs the same ruleset detection engine as the Screening page and returns a Chainalysis-compatible response (with our extensions). Sync (default) returns the full result; async returns a job_id to poll — mode=async responds with data:{job_id, type:'kya', status:'PENDING'}. Defaults to the builtin KYA ruleset when ruleset_id=0. Auth (required — anonymous calls are rejected): API key via ?apikey=, the X-Api-Key header, or Authorization: Bearer <JWT>. Rate limit: up to 300 concurrent in-flight requests per endpoint; beyond that a request waits up to 60s for a slot, then returns HTTP 429 {"code":429,"msg":"out of max connections"} — back off and retry, or use mode=async for high concurrency (result polling is not rate-limited).

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `chain_name` | string | yes | Blockchain network | Tron |
| `address` | string | yes | Wallet address to screen |  |
| `token` | string (usdt \| usdc) | no | Token type | usdt |
| `inflow_hops` | number | no | Inflow tracing depth (0–5). Send inflow_hops and outflow_hops both as 0 for a label-only screen: nothing is traced, only the address's own labels are matched against the ruleset's identity rules (max_hops = -1) | 3 |
| `outflow_hops` | number | no | Outflow tracing depth (0–5). Both hops 0 = label-only screen, see inflow_hops | 3 |
| `min_timestamp` | integer (Unix ms) | no | Start time filter (Unix ms). Omit for the full history. Narrowing the window makes the upstream investigation noticeably slower, so set it only when you need it | 0 |
| `max_timestamp` | integer (Unix ms) | no | End time filter (Unix ms). Default: current time | 0 |
| `min_amount` | number | no | Minimum transfer amount to consider | 10 |
| `max_nodes_per_hop` | number | no | Max nodes expanded per hop | 200 |
| `force_time_sequence` | boolean | no | Require every hop on a path to be no later than the one before it, so funds can only have moved forward in time. Paths are then read from the time-sequence layer, which is a strict subset of the raw one — expect fewer paths and a slower first run while the layer is built | true |
| `ruleset_id` | number | no | 0 = default builtin KYA ruleset | 0 |
| `scoring_ruleset_id` | number | no | Scoring ruleset driving the fund-attribution score (ruleset_type=scoring). 0 = the user's own scoring ruleset, else the builtin default | 0 |
| `cex_immune` | boolean | no | Treat a plain CEX (exchange) deposit address as clean: returns no hits and score 0 instead of screening its history. A CEX that also carries other high-risk labels (e.g. Sanctioned CEX, Sanctions, Mixers) is still screened normally | true |
| `force_run` | boolean | no | Re-run the investigation instead of reusing the one an identical earlier submit produced. gloomis dedups at the investigation level, so screening the same subject with the same parameters returns the earlier graph and misses anything that moved since. Costs a full investigation, so leave it off unless you need the chain re-read | false |
| `auto_monitor` | boolean | no | Screening adds nothing to monitoring by default. Set it to true to also put the screened address under continuous monitoring (one monitor per address and token, renewed by later screens). Omit it to follow your Monitoring settings switch (also off by default) | false |
| `mode` | string (sync \| async) | no | sync returns the full result immediately; async returns a job_id to poll via Get Screen Result | sync |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/screen/kya' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"chain_name":"Tron","address":"","token":"usdt","inflow_hops":3,"outflow_hops":3,"min_amount":10,"max_nodes_per_hop":200,"force_time_sequence":true,"ruleset_id":0,"scoring_ruleset_id":0,"cex_immune":true,"force_run":false,"auto_monitor":false,"mode":"sync"}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "address": "TSz3nv8...",
    "chain": "Tron",
    "score": {
      "score": 26.6,
      "verdict": "review",
      "selfHit": false,
      "components": [
        {
          "direction": "in",
          "hopBucket": "direct",
          "severity": "critical",
          "base": 80,
          "weight": 1,
          "amount": 185434.99,
          "rawAmount": 185434.99,
          "ratio": 0.266,
          "points": 26.6
        }
      ],
      "r1": 0.266,
      "r2": 0,
      "rOut": 0,
      "directAmount": 185434.99,
      "indirectAmount": 0,
      "outflowAmount": 0,
      "totalIn": 696674.38,
      "totalOut": 696674.38,
      "hitPaths": 24,
      "riskyEdges": 6
    },
    "scoreOverview": {
      "address": "TSz3nv8...",
      "token": "usdt",
      "inTotal": 696674.38,
      "outTotal": 696674.38,
      "inCount": 58,
      "outCount": 50,
      "balance": 0,
      "firstTs": 1694818383000,
      "lastTs": 1781415093000,
      "truncated": false
    },
    "subjectTags": [
      {
        "primaryCategory": "Sanctions",
        "tertiaryCategory": "OFAC"
      }
    ],
    "hits": [
      {
        "ruleCode": "KYA_SANCTION_EXPOSURE",
        "ruleName": "Exposure to sanctioned address",
        "category": "Sanctions",
        "riskLevel": "critical",
        "action": "block",
        "direction": "",
        "pathFlow": "inflow",
        "hops": 2,
        "opponentAddress": "T...",
        "maxAmount": 115776,
        "pathNodes": [
          {
            "address": "T...",
            "amount": 115776,
            "deep": 0,
            "tags": [
              {
                "primary_category": "Sanctions",
                "secondary_category": "OFAC",
                "tertiary_category": "SDN",
                "quaternary_category": "",
                "risk_level": "critical"
              }
            ]
          },
          {
            "address": "TSz3nv8...",
            "amount": 115776,
            "deep": 1,
            "tags": []
          }
        ],
        "exposureAmount": 115776,
        "riskRatePct": 16.6,
        "riskRateNumerator": 115776,
        "riskRateDenominator": 696674.38,
        "riskRateCapped": false,
        "riskRateGated": true,
        "highlighted": true,
        "highlightTags": [
          "Xinbi"
        ]
      }
    ],
    "highlightedHits": [
      {
        "ruleCode": "KYA_SANCTION_EXPOSURE",
        "ruleName": "Exposure to sanctioned address",
        "category": "Sanctions",
        "riskLevel": "critical",
        "action": "block",
        "direction": "",
        "pathFlow": "inflow",
        "hops": 2,
        "opponentAddress": "T...",
        "maxAmount": 115776,
        "pathNodes": [
          {
            "address": "T...",
            "amount": 115776,
            "deep": 0,
            "tags": [
              {
                "primary_category": "Sanctions",
                "secondary_category": "OFAC",
                "tertiary_category": "SDN",
                "quaternary_category": "",
                "risk_level": "critical"
              }
            ]
          },
          {
            "address": "TSz3nv8...",
            "amount": 115776,
            "deep": 1,
            "tags": []
          }
        ],
        "exposureAmount": 115776,
        "riskRatePct": 16.6,
        "riskRateNumerator": 115776,
        "riskRateDenominator": 696674.38,
        "riskRateCapped": false,
        "riskRateGated": true,
        "highlighted": true,
        "highlightTags": [
          "Xinbi"
        ]
      }
    ],
    "highlightSummary": {
      "rulesChecked": 3,
      "rulesHit": 1
    },
    "rulesTriggered": [
      "KYA_SANCTION_EXPOSURE"
    ],
    "rulesetId": 1,
    "totalPaths": 8,
    "hitPaths": 8,
    "inflowRiskAmount": 0,
    "inflowRiskRate": 0,
    "outflowRiskAmount": 0,
    "outflowRiskRate": 0,
    "risk": "critical",
    "riskScore": 90,
    "riskReason": "Critical: Exposure to sanctioned address (KYA_SANCTION_EXPOSURE)",
    "cluster": {
      "name": "",
      "category": ""
    },
    "addressType": "",
    "addressIdentifications": [
      {
        "category": "Sanctions",
        "name": "Address is a sanctioned entity",
        "description": "..."
      }
    ],
    "exposures": [
      {
        "category": "Sanctions",
        "direction": "inflow",
        "value": 68223
      },
      {
        "category": "Cybercrime",
        "direction": "inflow",
        "value": 53687
      }
    ],
    "snapshotId": 1038029
  }
}
```

**Response fields (data):**

- `address` (string) — The queried wallet address
- `chain` (string) — Blockchain network — see the chain_name options in the request panel for the current list
- `subjectTags` (object[]) — The screened address's own labels. Omitted when the address has none
  - `[].primaryCategory` (string) — Top-level label category (e.g. "Sanctions")
  - `[].tertiaryCategory` (string) — Third-level label category (e.g. "OFAC")
  - `[].riskLevel` (string) — The label's own risk level: low / medium / high / critical
- `risk` (string) — Our risk vocabulary: critical / high / medium / low
- `riskScore` (number) — Numeric risk score (0–100), mapped one-to-one from riskLevel (higher = riskier): critical = 90, high = 80, medium = 60, low = 10. It is 0 only when no rule is triggered (riskLevel then defaults to "low") — so riskLevel "low" with riskScore 0 means clean / no hits, whereas riskScore 10 means a low-severity rule fired.
- `riskReason` (string) — Human-readable summary of the highest-severity finding
- `cluster` (object) — Cluster attribution for the address (Chainalysis-style). Empty name/category when unresolved
- `addressType` (string) — Address entity type (e.g. exchange, mixer). Empty string if unresolved
- `addressIdentifications` (object[]) — What the address IS — identity-level findings derived from self-hits (e.g. the address itself is sanctioned)
  - `[].category` (string) — Identification category (e.g. "Sanctions")
  - `[].name` (string) — Short identification name
  - `[].description` (string) — Human-readable description
- `exposures` (object[]) — Aggregated exposure amounts by category and direction, derived from path hits
  - `[].category` (string) — Exposure category (e.g. "Sanctions", "Cybercrime")
  - `[].direction` (string) — Fund flow direction: inflow or outflow
  - `[].value` (number) — Exposure amount in token units
- `hits` (object[]) — Per-hit rule match detail with path evidence (our extension — not part of the Chainalysis spec)
  - `[].ruleCode` (string) — Unique rule code (e.g. "KYA_SANCTION_EXPOSURE")
  - `[].ruleName` (string) — Rule name
  - `[].category` (string) — Rule category (e.g. "Sanctions", "Cybercrime")
  - `[].riskLevel` (string) — Rule risk level: low / medium / high / critical
  - `[].action` (string) — Recommended action: block / review / alert / monitor
  - `[].direction` (string) — Reserved direction field (empty for KYA self/path hits)
  - `[].pathFlow` (string) — Path flow direction: inflow or outflow
  - `[].hops` (number) — Number of hops from the risk entity to the target address
  - `[].opponentAddress` (string) — The risk entity address at the other end of the path
  - `[].maxAmount` (number) — Maximum transfer amount on this path
  - `[].pathNodes` (object[]) — Ordered evidence chain from the opponent (deep 0) to the screened address. Keys inside a node stay snake_case
    - `[].address` (string) — Address at this hop
    - `[].amount` (number) — Transfer amount on the edge into this node (token units)
    - `[].deep` (number) — Hop index, 0 at the opponent
    - `[].last_closed_timestamp` (number) — When the edge into this node last moved funds (Unix ms). 0 or absent on the first node and whenever the upstream omits it
    - `[].tags` (object[]) — Every label on this node. Empty when the address has none
      - `[].primary_category` (string) — Top-level label category (e.g. "Sanctions")
      - `[].secondary_category` (string) — Second-level label category
      - `[].tertiary_category` (string) — Third-level label category (e.g. "OFAC")
      - `[].quaternary_category` (string) — Fourth-level label category
      - `[].risk_level` (string) — The label's own risk level: low / medium / high / critical
  - `[].exposureAmount` (number) — Risk funds that can traverse the whole path: the smallest edge on it (token units). This is the ratio numerator
  - `[].riskRatePct` (number) — exposureAmount / riskRateDenominator, as a percentage in 0–100
  - `[].riskRateNumerator` (number) — Smallest edge on the path — the same value as exposureAmount
  - `[].riskRateDenominator` (number) — The path's last edge: what the screened address received on an inflow, or what the risk address received on an outflow
  - `[].riskRateCapped` (boolean) — True only when the raw ratio exceeded 100%, which means the path data is inconsistent
  - `[].riskRateGated` (boolean) — True when the rule itself has a risk_rate condition. The ratio is computed for every hit; only gated rules should display it
  - `[].highlighted` (boolean) — Purely presentational (no effect on risk/action/score): true when the rule that produced this hit was flagged ★ by the ruleset owner. Absent on old results ⇒ false
  - `[].highlightTags` (string[]) — The rule's highlight_tags labels that matched this hit's counterparty label. Absent for an unscoped ★ (every hit highlighted) or on results predating this field
- `highlightedHits` (object[]) — Exact subset of hits with highlighted=true, pre-sorted by risk level (desc), then rule sort order, then hops (asc). Purely presentational — use it to surface ★ rules first without re-deriving/re-sorting the filter yourself
- `highlightSummary` (object) — How many of the ruleset's ★ highlighted rules were checked vs. hit for this screen. Absent/0 rulesChecked ⇒ the ruleset has no highlighted rules
  - `.rulesChecked` (number) — Highlighted rules evaluated
  - `.rulesHit` (number) — How many highlighted rules produced at least one highlighted hit (a scoped rule that fired without matching its highlight_tags counts toward rulesChecked but not here)
- `rulesTriggered` (string[]) — Rule codes triggered across all hits
- `rulesetId` (number) — The ruleset ID used for detection
- `totalPaths` (number) — Total number of fund paths analyzed
- `hitPaths` (number) — Number of paths that matched at least one rule
- `inflowRiskAmount` (number) — Contaminated inflow amount across unique hit inbound paths (token units)
- `inflowRiskRate` (number) — inflowRiskAmount / total inflow path amount, range 0–1
- `outflowRiskAmount` (number) — Risk outflow amount across unique hit outbound paths (token units)
- `outflowRiskRate` (number) — outflowRiskAmount / total outflow path amount, range 0–1
- `score` (object) — Fund-attribution risk score (资金占比评分). score = Σ (base × severity weight × fund ratio), where fund ratio is the deduped risky amount ÷ the address's total on-chain flow. Null when the graph had no flow totals.
  - `.score` (number) — 0–100 (null when denominators unavailable)
  - `.verdict` (string) — accept / review / edd / block (banded: 0-20 accept, 20-50 review, 50-80 edd, 80-100 block)
  - `.selfHit` (boolean) — True when the subject itself is flagged (score = self-hit score × severity weight)
  - `.selfHitLevel` (string) — Risk level of the self-hit (low / medium / high / critical). Omitted when selfHit is false
  - `.components[]` (object[]) — Per-cell breakdown: each = base × weight × ratio = points
  - `.r1 / .r2 / .rOut` (number) — Direct-in / indirect-in / out flow ratios (0–1)
  - `.totalIn / .totalOut` (number) — The on-chain token volume used as the denominator
  - `.hitPaths / .riskyEdges` (number) — Hit path count / deduped risky edge count
- `scoreOverview` (object) — The address's on-chain activity behind the score: full token volume (the denominator) + tx counts + balance + activity window. Absent when the chain-volume fetch failed.
  - `.address` (string) — The screened address
  - `.token` (string) — Token (usdt / usdc)
  - `.inTotal / .outTotal` (number) — Total token inflow / outflow
  - `.inCount / .outCount` (number) — Transfer counts in / out
  - `.balance` (number) — Current token balance (null when unavailable)
  - `.firstTs / .lastTs` (number) — Earliest / latest transfer timestamp (ms). Either is omitted when the provider did not return it
  - `.truncated` (boolean) — True when the volume hit the provider page cap — the ratio is then conservative. Omitted when false
- `snapshotId` (number) — Sync responses: id of the snapshot this screen was recorded in — read it back any time with Get Snapshot (GET /api/v3/snapshots/{snapshotId}). An async job reports it as snapshot_id on Get Screen Result


### KYT Screen (v3)

`POST https://test-api.width.live/api/v3/screen/kyt`

Chainalysis-aligned transaction screening. screen_direction (In / Out / Both, matching the Screening page) selects which endpoint(s) to screen: in (source-of-funds), out (destination), or both (default — both endpoints, merged). Rulesets are per-direction — in_ruleset_id applies to the in/source side and out_ruleset_id to the out/destination side, so "both" can use a different ruleset on each side (each defaults to its KYT-IN / KYT-OUT builtin). Sync/async like KYA Screen (v3). One screen covers one transfer: a transaction can move the token several times (a batch payout, a DEX route), and event_index — or from_address / to_address — names the one to screen; List Transaction Transfers shows them. With none named the first transfer is screened, and the same one again on every later call for that transaction, so a repeated request gets a repeated answer; transfer in the response says which one it was. A name that matches no transfer, or several, returns HTTP 400 with the transaction's transfers under data.transfers. Rate limit: up to 300 concurrent in-flight requests per endpoint; beyond that a request waits up to 60s for a slot, then returns HTTP 429 {"code":429,"msg":"out of max connections"} — back off and retry, or use mode=async for high concurrency (result polling is not rate-limited).

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `chain_name` | string | yes | Blockchain network | Tron |
| `token` | string (usdt \| usdc) | no | Token type. Omitted when the transfer is named (event_index, or from_address / to_address), that transfer's own token is used; otherwise the chain's default | usdt |
| `tx_id` | string | yes | Transaction hash to screen |  |
| `event_index` | number | no | Which transfer of the transaction to screen: its event index as an explorer shows it — the logIndex on EVM chains, the log position inside the transaction on Tron (0 is a valid index). Omit to screen the first transfer |  |
| `from_address` | string | no | Pick the transfer by its sender instead of event_index (with to_address, or alone when it identifies one transfer) |  |
| `to_address` | string | no | Pick the transfer by its recipient instead of event_index |  |
| `screen_direction` | string (both \| in \| out) | no | in = incoming/source (KYT-IN), out = outgoing/destination (KYT-OUT), both = screen both endpoints and merge | both |
| `inflow_hops` | number | no | Inflow tracing depth (0–5). Send inflow_hops and outflow_hops both as 0 for a label-only screen: nothing is traced, only the screened endpoints' own labels are matched against the ruleset's identity rules (max_hops = -1) | 3 |
| `outflow_hops` | number | no | Outflow tracing depth (0–5). Both hops 0 = label-only screen, see inflow_hops | 3 |
| `min_timestamp` | integer (Unix ms) | no | Start time filter (Unix ms). Omit for the full history. Narrowing the window makes the upstream investigation noticeably slower, so set it only when you need it | 0 |
| `max_timestamp` | integer (Unix ms) | no | End time filter (Unix ms). Default: current time | 0 |
| `min_amount` | number | no | Minimum transfer amount to consider | 10 |
| `max_nodes_per_hop` | number | no | Max nodes expanded per hop | 200 |
| `force_time_sequence` | boolean | no | Require every hop on a path to be no later than the one before it, so funds can only have moved forward in time. Paths are then read from the time-sequence layer, which is a strict subset of the raw one — expect fewer paths and a slower first run while the layer is built | true |
| `in_ruleset_id` | number | no | Ruleset for the in/source screen (used by in + both). 0 = ruleset_id fallback, else KYT-IN builtin | 0 |
| `out_ruleset_id` | number | no | Ruleset for the out/destination screen (used by out + both). 0 = ruleset_id fallback, else KYT-OUT builtin | 0 |
| `ruleset_id` | number | no | Generic fallback for whichever side has no specific id. 0 = per-side builtin | 0 |
| `scoring_ruleset_id` | number | no | Scoring ruleset driving the fund-attribution score (ruleset_type=scoring). 0 = the user's own scoring ruleset, else the builtin default | 0 |
| `cex_immune` | boolean | no | Treat a plain CEX (exchange) endpoint as clean: when the screened source/destination is a known CEX deposit address with no other high-risk labels, that side returns no hits and score 0 instead of screening its history. A CEX that also carries other high-risk labels is still screened | true |
| `force_run` | boolean | no | Re-run the investigation instead of reusing the one an identical earlier submit produced. gloomis dedups at the investigation level, so screening the same subject with the same parameters returns the earlier graph and misses anything that moved since. Costs a full investigation, so leave it off unless you need the chain re-read | false |
| `auto_monitor` | boolean | no | Screening adds nothing to monitoring by default. Set it to true to also put the screened endpoint address(es) under continuous monitoring — the source for screen_direction=in, the destination for out, both for both. Omit it to follow your Monitoring settings switch (also off by default) | false |
| `mode` | string (sync \| async) | no | sync returns the full result immediately; async returns a job_id to poll via Get Screen Result | sync |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/screen/kyt' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"chain_name":"Tron","token":"usdt","tx_id":"","screen_direction":"both","inflow_hops":3,"outflow_hops":3,"min_amount":10,"max_nodes_per_hop":200,"force_time_sequence":true,"in_ruleset_id":0,"out_ruleset_id":0,"ruleset_id":0,"scoring_ruleset_id":0,"cex_immune":true,"force_run":false,"auto_monitor":false,"mode":"sync"}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "transaction": "a1b2c3...",
    "chain": "Tron",
    "transfer": {
      "eventIndex": 3,
      "eventUid": "tron:usdt:a1b2c3...:3",
      "token": "usdt",
      "from": "TX69w...",
      "to": "TWTZ...",
      "amount": 2766.01599,
      "rawAmount": "2766015990"
    },
    "transferSelection": "explicit",
    "transferCount": 2,
    "transfers": [
      {
        "eventIndex": 0,
        "eventUid": "tron:usdt:a1b2c3...:0",
        "token": "usdt",
        "from": "TEZW...",
        "to": "TX69w...",
        "amount": 2766.01599,
        "rawAmount": "2766015990"
      },
      {
        "eventIndex": 3,
        "eventUid": "tron:usdt:a1b2c3...:3",
        "token": "usdt",
        "from": "TX69w...",
        "to": "TWTZ...",
        "amount": 2766.01599,
        "rawAmount": "2766015990"
      }
    ],
    "score": {
      "score": 40,
      "verdict": "review",
      "selfHit": false,
      "components": [
        {
          "direction": "in",
          "hopBucket": "hop2",
          "severity": "high",
          "base": 50,
          "weight": 0.8,
          "amount": 4194296.43,
          "rawAmount": 4194296.43,
          "ratio": 0.784,
          "points": 31.4
        }
      ],
      "r1": 0,
      "r2": 1,
      "rOut": 0,
      "directAmount": 0,
      "indirectAmount": 4194296.43,
      "outflowAmount": 0,
      "totalIn": 5349043.05,
      "totalOut": 6035080.35,
      "hitPaths": 33,
      "riskyEdges": 15
    },
    "inScore": {
      "score": 40,
      "verdict": "review",
      "selfHit": false,
      "totalIn": 5349043.05,
      "totalOut": 6035080.35
    },
    "outScore": {
      "score": 26.6,
      "verdict": "review",
      "selfHit": false,
      "totalIn": 696674.38,
      "totalOut": 696674.38
    },
    "inScoreOverview": {
      "address": "TX69w...",
      "token": "usdt",
      "inTotal": 5349043.05,
      "outTotal": 6035080.35,
      "inCount": 2834,
      "outCount": 2166,
      "balance": 0,
      "firstTs": 1680000000000,
      "lastTs": 1780000000000,
      "truncated": true
    },
    "outScoreOverview": {
      "address": "TWTZ...",
      "token": "usdt",
      "inTotal": 696674.38,
      "outTotal": 696674.38,
      "inCount": 58,
      "outCount": 50,
      "balance": 0,
      "firstTs": 1694818383000,
      "lastTs": 1781415093000,
      "truncated": false
    },
    "hits": [
      {
        "ruleCode": "KYT_IN_SANCTION_SOURCE",
        "ruleName": "Funds sourced from sanctioned party",
        "category": "Sanctions",
        "riskLevel": "critical",
        "action": "block",
        "direction": "inbound",
        "pathFlow": "inflow",
        "hops": 2,
        "opponentAddress": "T...",
        "maxAmount": 1234.5,
        "pathNodes": [
          {
            "address": "T...",
            "amount": 1234.5,
            "deep": 0,
            "tags": [
              {
                "primary_category": "Sanctions",
                "secondary_category": "OFAC",
                "tertiary_category": "SDN",
                "quaternary_category": "",
                "risk_level": "critical"
              }
            ]
          },
          {
            "address": "TX69w...",
            "amount": 1234.5,
            "deep": 1,
            "tags": []
          }
        ],
        "exposureAmount": 1234.5,
        "riskRatePct": 0.02,
        "riskRateNumerator": 1234.5,
        "riskRateDenominator": 5349043.05,
        "riskRateCapped": false,
        "riskRateGated": true,
        "highlighted": true,
        "highlightTags": [
          "Xinbi"
        ]
      }
    ],
    "highlightedHits": [
      {
        "ruleCode": "KYT_IN_SANCTION_SOURCE",
        "ruleName": "Funds sourced from sanctioned party",
        "category": "Sanctions",
        "riskLevel": "critical",
        "action": "block",
        "direction": "inbound",
        "pathFlow": "inflow",
        "hops": 2,
        "opponentAddress": "T...",
        "maxAmount": 1234.5,
        "pathNodes": [
          {
            "address": "T...",
            "amount": 1234.5,
            "deep": 0,
            "tags": [
              {
                "primary_category": "Sanctions",
                "secondary_category": "OFAC",
                "tertiary_category": "SDN",
                "quaternary_category": "",
                "risk_level": "critical"
              }
            ]
          },
          {
            "address": "TX69w...",
            "amount": 1234.5,
            "deep": 1,
            "tags": []
          }
        ],
        "exposureAmount": 1234.5,
        "riskRatePct": 0.02,
        "riskRateNumerator": 1234.5,
        "riskRateDenominator": 5349043.05,
        "riskRateCapped": false,
        "riskRateGated": true,
        "highlighted": true,
        "highlightTags": [
          "Xinbi"
        ]
      }
    ],
    "inHighlightSummary": {
      "rulesChecked": 2,
      "rulesHit": 1
    },
    "outHighlightSummary": {
      "rulesChecked": 2,
      "rulesHit": 0
    },
    "rulesTriggered": [
      "KYT_IN_SANCTION_SOURCE"
    ],
    "rulesetId": 2,
    "totalPaths": 5,
    "hitPaths": 5,
    "risk": "critical",
    "riskScore": 90,
    "alerts": [
      {
        "alertLevel": "critical",
        "category": "Sanctions",
        "service": "KYT",
        "exposureType": "INDIRECT",
        "alertAmount": 1234.5,
        "categoryId": "KYT_IN_SANCTION_SOURCE",
        "direction": "inbound",
        "hops": 2,
        "opponentAddress": "T...",
        "action": "block",
        "highlighted": true,
        "highlightTags": [
          "Xinbi"
        ]
      }
    ],
    "snapshotId": 1024198
  }
}
```

**Response fields (data):**

- `transaction` (string) — Transaction hash
- `transfer` (object) — The transfer screened. Absent only on results recorded before transfers were tracked
  - `.eventIndex` (number) — Its event index: the EVM logIndex, or the log position inside a Tron transaction
  - `.eventUid` (string) — Stable id of the event: {chain}:{token}:{tx}:{eventIndex}
  - `.token` (string) — The token screened
  - `.from / .to` (string) — Sender and recipient — the source and destination endpoints the screen covers
  - `.amount` (number) — Amount of this transfer in whole tokens (not the transaction's total)
  - `.rawAmount` (string) — The same amount in base units, as a string (18 decimals on BSC, 6 elsewhere)
- `transferSelection` (string) — explicit when the request named the transfer (event_index / from_address / to_address); default when it did not and the first transfer — or the one an earlier unnamed screen of this transaction took — was screened
- `transferCount` (number) — How many transfers of the token the transaction carries. Above 1, check transfer is the one you meant
- `transfers` (object[]) — The transaction's transfers of the token in event order, at most 100. Same shape as transfer
- `transfersTruncated` (boolean) — True when transfers was cut at 100. Omitted when false
- `risk` (string) — Our risk vocabulary: critical / high / medium / low
- `riskScore` (number) — Numeric risk score (0–100), mapped one-to-one from riskLevel (higher = riskier): critical = 90, high = 80, medium = 60, low = 10. It is 0 only when no rule is triggered (riskLevel then defaults to "low") — so riskLevel "low" with riskScore 0 means clean / no hits, whereas riskScore 10 means a low-severity rule fired.
- `alerts` (object[]) — Chainalysis KYT-compatible alert list
  - `[].alertLevel` (string) — Alert severity: low / medium / high / critical
  - `[].category` (string) — Alert category (e.g. "Sanctions")
  - `[].service` (string) — Originating service, always "KYT" for this endpoint
  - `[].exposureType` (string) — DIRECT (hops=0/1) or INDIRECT (hops>1), derived from hop count
  - `[].alertAmount` (number) — Amount associated with this alert (token units)
  - `[].categoryId` (string) — Rule code that produced this alert (e.g. "KYT_IN_SANCTION_SOURCE")
  - `[].direction` (string) — Our extension: inbound or outbound
  - `[].hops` (number) — Our extension: number of hops from the risk entity
  - `[].opponentAddress` (string) — Our extension: the risk entity address at the other end of the path
  - `[].action` (string) — Our extension: recommended action — block / review / alert / monitor
  - `[].highlighted` (boolean) — Our extension, purely presentational (no effect on risk/action/score): true when the rule that produced this alert was flagged ★ by the ruleset owner
  - `[].highlightTags` (string[]) — Our extension: the rule's highlight_tags labels that matched this alert's counterparty label. Absent for an unscoped ★
- `hits` (object[]) — Per-hit rule match detail with path evidence (our extension — not part of the Chainalysis spec)
  - `[].ruleCode` (string) — Unique rule code (e.g. "KYT_IN_SANCTION_SOURCE")
  - `[].ruleName` (string) — Rule name
  - `[].category` (string) — Rule category (e.g. "Sanctions")
  - `[].riskLevel` (string) — Rule risk level: low / medium / high / critical
  - `[].action` (string) — Recommended action: block / review / alert / monitor
  - `[].direction` (string) — Path direction: inbound or outbound
  - `[].pathFlow` (string) — Path flow direction: inflow or outflow
  - `[].hops` (number) — Number of hops from the risk entity to the target address
  - `[].opponentAddress` (string) — The risk entity address at the other end of the path
  - `[].maxAmount` (number) — Maximum transfer amount on this path
  - `[].pathNodes` (object[]) — Ordered evidence chain from the opponent (deep 0) to the screened endpoint. Same shape as the KYA hit pathNodes: address, amount, deep, last_closed_timestamp, and tags[] (primary_category, secondary_category, tertiary_category, quaternary_category, risk_level)
  - `[].exposureAmount` (number) — Risk funds that can traverse the whole path: the smallest edge on it (token units). This is the ratio numerator
  - `[].riskRatePct` (number) — exposureAmount / riskRateDenominator, as a percentage in 0–100
  - `[].riskRateNumerator` (number) — Smallest edge on the path — the same value as exposureAmount
  - `[].riskRateDenominator` (number) — The path's last edge: what the screened endpoint received on an inflow, or what the risk address received on an outflow
  - `[].riskRateCapped` (boolean) — True only when the raw ratio exceeded 100%, which means the path data is inconsistent
  - `[].riskRateGated` (boolean) — True when the rule itself has a risk_rate condition. The ratio is computed for every hit; only gated rules should display it
  - `[].highlighted` (boolean) — Purely presentational (no effect on risk/action/score): true when the rule that produced this hit was flagged ★ by the ruleset owner
  - `[].highlightTags` (string[]) — The rule's highlight_tags labels that matched this hit's counterparty label. Absent for an unscoped ★ (every hit highlighted)
- `highlightedHits` (object[]) — Exact subset of hits with highlighted=true, pre-sorted by risk level (desc), then rule sort order, then hops (asc). screen_direction=both concatenates both sides — tell them apart by which endpoint a hit's pathNodes pass through (inScoreOverview.address is the source, outScoreOverview.address the destination)
- `highlightSummary` (object) — screen_direction=in/out only: how many of the ruleset's ★ highlighted rules were checked vs. hit. Absent/0 rulesChecked ⇒ no highlighted rules on that side's ruleset
  - `.rulesChecked` (number) — Highlighted rules evaluated
  - `.rulesHit` (number) — How many highlighted rules produced at least one highlighted hit (a scoped rule that fired without matching its highlight_tags counts toward rulesChecked but not here)
- `inHighlightSummary` (object) — screen_direction=both only: the in/source side's highlight summary. Same shape as highlightSummary
- `outHighlightSummary` (object) — screen_direction=both only: the out/destination side's highlight summary. Same shape as highlightSummary
- `rulesTriggered` (string[]) — Rule codes triggered across all hits
- `rulesetId` (number) — The ruleset ID used for detection (KYT-IN or KYT-OUT depending on screen_direction). 0 when screen_direction=both — use inRulesetId and outRulesetId
- `inRulesetId` (number) — The ruleset applied to the in/source screen. Set for screen_direction=in (equal to rulesetId) and both; omitted for out
- `outRulesetId` (number) — The ruleset applied to the out/destination screen. Set for screen_direction=out (equal to rulesetId) and both; omitted for in
- `totalPaths` (number) — Total number of fund paths analyzed
- `hitPaths` (number) — Number of paths that matched at least one rule
- `score` (object) — Fund-attribution risk score (资金占比评分), the higher of the two sides for screen_direction=both. Same shape as the KYA score.
  - `.score` (number) — 0–100 (null when denominators unavailable)
  - `.verdict` (string) — accept / review / edd / block
  - `.selfHit` (boolean) — True when the screened endpoint itself is flagged
  - `.selfHitLevel` (string) — Risk level of the self-hit. Omitted when selfHit is false
  - `.components[]` (object[]) — Per-cell breakdown: base × weight × ratio = points
  - `.totalIn / .totalOut` (number) — The on-chain token volume used as the denominator
- `inScore` (object) — screen_direction=both only: the source (from) endpoint's own score. Same shape as score
- `outScore` (object) — screen_direction=both only: the destination (to) endpoint's own score. Same shape as score
- `subjectTags` (object[]) — The screened endpoint's own labels (primaryCategory, tertiaryCategory, riskLevel). Single-direction screens only — screen_direction=both clears this and uses fromTags / toTags. Omitted when the address has no labels
- `fromTags` (object[]) — screen_direction=both only: the source endpoint's own labels. Same shape as subjectTags
- `toTags` (object[]) — screen_direction=both only: the destination endpoint's own labels. Same shape as subjectTags
- `scoreOverview` (object) — The address overview behind the combined score (the higher side's). Same shape as the KYA scoreOverview
  - `.inTotal / .outTotal` (number) — Total token inflow / outflow
  - `.inCount / .outCount` (number) — Transfer counts in / out
  - `.balance` (number) — Current token balance (null when unavailable)
  - `.firstTs / .lastTs` (number) — Earliest / latest transfer timestamp (ms). Either is omitted when the provider did not return it
  - `.truncated` (boolean) — True when the volume hit the provider page cap. Omitted when false
- `inScoreOverview` (object) — screen_direction=both only: the source endpoint's address overview. Same shape as scoreOverview
- `outScoreOverview` (object) — screen_direction=both only: the destination endpoint's address overview. Same shape as scoreOverview
- `snapshotId` (number) — Sync responses: id of the snapshot this screen was recorded in — read it back any time with Get Snapshot (GET /api/v3/snapshots/{snapshotId}). An async job reports it as snapshot_id on Get Screen Result


### List Transaction Transfers (v3)

`POST https://test-api.width.live/api/v3/transaction-transfers`

The token transfers inside one transaction, in event order — the list to pick KYT Screen's event_index from. A batch payout or a DEX route moves a token many times in one transaction, and a KYT screen covers one of those transfers. Omit token to list every token screening carries. A transaction too new for the index is read from a node instead (source: node). Not found is HTTP 404; a lookup that fails upstream is HTTP 502, worth retrying; a token the transaction does not move is HTTP 400 listing what it does move. Auth: API key, same as the screening endpoints; not charged.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `chain_name` | string | yes | Blockchain network | Ethereum |
| `tx_id` | string | yes | Transaction hash |  |
| `token` | string | no | Only this token (usdt / usdc). Omit for every token |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/transaction-transfers' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"chain_name":"Ethereum","tx_id":""}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "chain": "Ethereum",
    "transaction": "0xf4685f8b...",
    "blockNumber": 26180580,
    "blockTimestamp": 1791858467000,
    "transferCount": 2,
    "transfers": [
      {
        "eventIndex": 123,
        "eventUid": "ethereum:usdc:0xf4685f8b...:123",
        "token": "usdc",
        "from": "0x70b324b9...",
        "to": "0xd8c8a2b1...",
        "amount": 2.12,
        "rawAmount": "2120000"
      },
      {
        "eventIndex": 127,
        "eventUid": "ethereum:usdc:0xf4685f8b...:127",
        "token": "usdc",
        "from": "0x70b324b9...",
        "to": "0x00000000000004444c5d...",
        "amount": 17.88,
        "rawAmount": "17880000"
      }
    ],
    "truncated": false,
    "source": "indexer"
  }
}
```

**Response fields (data):**

- `transferCount` (number) — Transfers listed
- `transfers` (object[]) — One per Transfer event, in event order: eventIndex (pass it to KYT Screen), eventUid, token, from, to, amount (whole tokens), rawAmount (base units)
- `blockNumber / blockTimestamp` (number) — Block of the transaction; the timestamp is in ms
- `truncated` (boolean) — True when the transaction has more than 5000 transfer events and only the first 5000 are listed
- `source` (string) — indexer, or node for a transaction the index has not reached yet


### Get Screen Result (async)

`GET https://test-api.width.live/api/v3/screen/result/{jobId}`

Poll an async screening job. Every screening call gets its own job_id — even two calls on the same address or transaction — and a job_id only ever returns the result of the call that created it. The job id is all you need: the submit call's parameters (chain, subject, token, ruleset, KYT direction, cex_immune) are recorded server-side and are the only ones used — query parameters cannot change a job's result — and the job type is inferred. Returns status PENDING/PROCESSING/COMPLETE with the result under result when complete; a completed job keeps returning that same result. This endpoint never blocks: once the upstream investigation finishes, detection is started in the background and the call still answers PROCESSING — keep polling (a few seconds apart) until COMPLETE. Detection runs once per job, so polling costs nothing extra. A job_id of 1,000,000,000 or above (every id issued per request) that is unknown, or belongs to another account, returns HTTP 404; older ids from before per-request job ids go through the legacy lookup. A sync screening that times out returns HTTP 500 whose data carries a job_id: poll that job_id for the result rather than resubmitting, because resubmitting creates and charges a new job.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `jobId` | number | yes | The job_id from an async submit (path parameter) |  |

**Example request:**

```bash
curl 'https://test-api.width.live/api/v3/screen/result/<jobId>' -H 'X-Api-Key: $DART_API_KEY'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "job_id": 1000004821,
    "type": "kya",
    "status": "COMPLETE",
    "result": {
      "address": "TSz3nv8...",
      "chain": "Tron",
      "risk": "critical",
      "riskScore": 90,
      "riskReason": "Critical: Exposure to sanctioned address (KYA_SANCTION_EXPOSURE)",
      "cluster": {
        "name": "",
        "category": ""
      },
      "addressType": "",
      "addressIdentifications": [
        {
          "category": "Sanctions",
          "name": "Address is a sanctioned entity",
          "description": "..."
        }
      ],
      "exposures": [
        {
          "category": "Sanctions",
          "direction": "inflow",
          "value": 68223
        }
      ],
      "hits": [
        {
          "ruleCode": "KYA_SANCTION_EXPOSURE",
          "ruleName": "Exposure to sanctioned address",
          "category": "Sanctions",
          "riskLevel": "critical",
          "action": "block",
          "direction": "",
          "pathFlow": "inflow",
          "hops": 2,
          "opponentAddress": "T...",
          "maxAmount": 115776,
          "pathNodes": [
            {
              "address": "T...",
              "amount": 115776,
              "deep": 0,
              "tags": [
                {
                  "primary_category": "Sanctions",
                  "secondary_category": "OFAC",
                  "tertiary_category": "SDN",
                  "quaternary_category": "",
                  "risk_level": "critical"
                }
              ]
            },
            {
              "address": "TSz3nv8...",
              "amount": 115776,
              "deep": 1,
              "tags": []
            }
          ],
          "exposureAmount": 115776,
          "riskRatePct": 16.6,
          "riskRateNumerator": 115776,
          "riskRateDenominator": 696674.38,
          "riskRateCapped": false,
          "riskRateGated": true,
          "highlighted": true,
          "highlightTags": [
            "Xinbi"
          ]
        }
      ],
      "highlightedHits": [],
      "highlightSummary": {
        "rulesChecked": 3,
        "rulesHit": 1
      },
      "rulesTriggered": [
        "KYA_SANCTION_EXPOSURE"
      ],
      "rulesetId": 1,
      "totalPaths": 8,
      "hitPaths": 8,
      "inflowRiskAmount": 0,
      "inflowRiskRate": 0,
      "outflowRiskAmount": 0,
      "outflowRiskRate": 0
    },
    "snapshot_id": 1038031
  }
}
```

**Response fields (data):**

- `job_id` (number) — The polled job ID
- `type` (string) — Screening type this job was submitted for: kya or kyt
- `status` (string) — Job status: PENDING / PROCESSING / COMPLETE
- `result` (object) — Present when status=COMPLETE. Same KYA/KYT result object as the sync KYA Screen (v3) / KYT Screen (v3) endpoints
- `snapshot_id` (number) — Present when status=COMPLETE: id of the snapshot the result was recorded in — read it back any time with Get Snapshot (GET /api/v3/snapshots/{snapshotId})


### Get Snapshot (v3)

`GET https://test-api.width.live/api/v3/snapshots/{snapshotId}`

Read back a recorded screening by its snapshot id: the parameters it ran with and its result. Snapshot ids come from a sync screen (snapshotId), a completed async job (snapshot_id on Get Screen Result), monitoring alerts, and the console's snapshot pages. Only your own snapshots — an id that does not exist or belongs to another account answers HTTP 404. A snapshot made through this API returns exactly the result the API answered; KYA snapshots from the console, monitoring and the v2 API come back in the same KYA result shape, as the console's API JSON view shows them. Snapshots with no such shape (KYT screens from the console, Merkle Science reports) answer HTTP 422. Read-only and not charged.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `snapshotId` | number | yes | The snapshot id (path parameter) |  |

**Example request:**

```bash
curl 'https://test-api.width.live/api/v3/snapshots/<snapshotId>' -H 'X-Api-Key: $DART_API_KEY'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "snapshot_id": 1038031,
    "job_id": 1000004821,
    "type": "kya",
    "source": "api_v3",
    "chain": "Tron",
    "subject": "TSz3nv8...",
    "score": 82,
    "verdict": "block",
    "risk_level": "critical",
    "duration_ms": 5321,
    "created_at": "2026-10-14T16:53:28.276859+08:00",
    "parameters": {
      "chain_name": "Tron",
      "address": "TSz3nv8...",
      "token": "usdt",
      "scenario": "all",
      "ruleset_id": 22,
      "cex_immune": true,
      "inflow_hops": 3,
      "outflow_hops": 1,
      "max_timestamp": 1791881608070,
      "min_amount": 10,
      "max_nodes_per_hop": 200,
      "force_time_sequence": true,
      "mode": "async"
    },
    "result": {
      "address": "TSz3nv8...",
      "chain": "Tron",
      "score": {
        "score": 82,
        "verdict": "block",
        "selfHit": false
      },
      "hits": [
        {
          "ruleCode": "KYA_SANCTION_EXPOSURE",
          "ruleName": "Exposure to sanctioned address",
          "riskLevel": "critical",
          "action": "block",
          "hops": 2,
          "exposureAmount": 115776
        }
      ],
      "rulesTriggered": [
        "KYA_SANCTION_EXPOSURE"
      ],
      "rulesetId": 22,
      "totalPaths": 8,
      "hitPaths": 3,
      "risk": "critical",
      "riskScore": 90
    }
  }
}
```

**Response fields (data):**

- `snapshot_id` (number) — The snapshot id
- `job_id` (number) — The job the screen ran as. API screens only — absent for a sync screen of a plain exchange address (no job runs) and for console or monitoring snapshots
- `type` (string) — kya (address) or kyt (transaction)
- `source` (string) — Where the screen came from: api_v3 (this API), screening (the console), monitoring, api_v2
- `chain` (string) — Blockchain network
- `subject` (string) — The screened address (kya) or transaction hash (kyt)
- `score` (number) — Fund-attribution score 0–100; null when the result has none
- `verdict` (string) — accept / review / edd / block
- `risk_level` (string) — Our risk vocabulary: critical / high / medium / low
- `duration_ms` (number) — Screening wall-clock time in ms. Omitted when it was not measured
- `created_at` (string) — When the screen was recorded (RFC 3339)
- `parameters` (object) — The parameters the screen ran with. API screens: the request after defaults were resolved. Console and monitoring: their own request, without the raw path data
- `result` (object) — The screening result — same shape as KYA Screen (v3) / KYT Screen (v3)


## v3 Rulesets & Rules

### List Rulesets

`POST https://test-api.width.live/api/v3/rulesets/list`

Every ruleset the caller can see: their own plus the builtins (AML KYA Default, KYT-IN, KYT-OUT, the scoring default). An empty body lists them all; ruleset_type keeps one kind, ids narrows to specific rulesets, include_builtin=false leaves only your own. Each item carries rules_count and highlighted_rules_count; is_builtin tells you which ones are read-only. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). Nothing is charged.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_type` | string (kya \| kyt_in \| kyt_out \| scoring) | no | Keep one kind: kya (address screening), kyt_in / kyt_out (transaction source / destination), scoring (fund-attribution score). Empty = all |  |
| `include_builtin` | boolean | no | false returns only the rulesets you created or cloned | true |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/list' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"include_builtin":true}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": [
    {
      "id": 22,
      "name": "AML KYA Default v1.3",
      "jurisdiction": "Global",
      "is_builtin": true,
      "generated_by": "kya-v2.1-hilite",
      "status": "ready",
      "ruleset_type": "kya",
      "rules_count": 72,
      "highlighted_rules_count": 5,
      "created_at": "2026-07-16T10:02:11Z"
    },
    {
      "id": 29,
      "name": "AML KYA Default v1.3 (Copy)",
      "jurisdiction": "Global",
      "is_builtin": false,
      "generated_by": "kya-v2.1-hilite",
      "status": "ready",
      "ruleset_type": "kya",
      "rules_count": 72,
      "highlighted_rules_count": 8,
      "created_at": "2026-09-08T18:05:40Z"
    }
  ]
}
```

**Response fields (data):**

- `[].id` (number) — Ruleset id — the ruleset_id / scoring_ruleset_id you pass to the screening endpoints
- `[].ruleset_type` (string) — kya, kyt_in, kyt_out or scoring
- `[].is_builtin` (boolean) — Builtin rulesets are read-only; clone one to edit
- `[].rules_count` (number) — Live rules in the ruleset
- `[].highlighted_rules_count` (number) — Rules flagged as highlighted (★)


### Get Ruleset

`POST https://test-api.width.live/api/v3/rulesets/get`

One ruleset with all of its rules inline — the same shape rule/list returns per rule, plus scoring_config for a scoring ruleset. You see your own rulesets and the builtins; only your own can be changed — a builtin answers 403, clone it first. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_id` | number | yes | Ruleset id | 22 |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/get' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"ruleset_id":22}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 29,
    "name": "AML KYA Default v1.3 (Copy)",
    "jurisdiction": "Global",
    "icon": "",
    "is_builtin": false,
    "generated_by": "kya-v2.1-hilite",
    "status": "ready",
    "ruleset_type": "kya",
    "source_policy_ids": [],
    "rules": [
      {
        "id": 2101,
        "ruleset_id": 29,
        "rule_code": "KYA_SANCTION_SELFHIT",
        "category": "Sanctions",
        "name": "Address is a sanctioned entity",
        "description": "Address itself carries a Sanctions label.",
        "risk_level": "critical",
        "action": "block",
        "direction": "both",
        "min_hops": 0,
        "max_hops": -1,
        "reference": "",
        "conditions": [
          {
            "parameter": "primary_category",
            "operator": "==",
            "value": "Sanctions"
          }
        ],
        "sort_order": 1,
        "is_highlighted": true,
        "highlight_tags": []
      },
      {
        "id": 2140,
        "ruleset_id": 29,
        "rule_code": "KYA_SANCTION_EXPOSURE_H2",
        "category": "Sanctions",
        "name": "Exposure to sanctioned address (2 hops)",
        "description": "",
        "risk_level": "critical",
        "action": "block",
        "direction": "both",
        "min_hops": 2,
        "max_hops": 2,
        "reference": "",
        "conditions": [
          {
            "parameter": "primary_category",
            "operator": "==",
            "value": "Sanctions"
          },
          {
            "parameter": "risk_rate",
            "operator": ">=",
            "value": 5
          }
        ],
        "sort_order": 40,
        "is_highlighted": false,
        "highlight_tags": [
          "Xinbi"
        ]
      }
    ],
    "created_at": "2026-09-08T18:05:40Z",
    "updated_at": "2026-09-10T19:30:02Z"
  }
}
```

**Response fields (data):**

- `rules[]` (array) — The ruleset's rules, in sort_order — see rule/get for the fields
- `scoring_config` (object) — Only on a scoring ruleset: formula bases, severity weights and verdict bands


### Create Ruleset

`POST https://test-api.width.live/api/v3/rulesets/create`

An empty ruleset of your own. ruleset_type is required and decides where it can be used: kya for address screening (ruleset_id on screen/kya), kyt_in / kyt_out for the two sides of a transaction screen, scoring for a fund-attribution scoring ruleset (scoring_ruleset_id) — a scoring ruleset holds a scoring_config instead of rules. Most integrations clone a builtin instead and edit the copy. Returns the created ruleset. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `name` | string | yes | Display name | My KYA ruleset |
| `ruleset_type` | string (kya \| kyt_in \| kyt_out \| scoring) | yes | kya, kyt_in, kyt_out or scoring | kya |
| `jurisdiction` | string | no | Free text, e.g. Global or SG | Global |
| `scoring_config` | array (JSON) | no | Scoring rulesets only: the config object (bases, severity weights, verdict bands). Omit to start from the default |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/create' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"name":"My KYA ruleset","ruleset_type":"kya","jurisdiction":"Global","scoring_config":[]}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 41,
    "name": "My KYA ruleset",
    "jurisdiction": "Global",
    "icon": "",
    "is_builtin": false,
    "generated_by": "api",
    "status": "ready",
    "ruleset_type": "kya",
    "source_policy_ids": [],
    "rules": [],
    "created_at": "2026-09-11T09:12:44Z",
    "updated_at": "2026-09-11T09:12:44Z"
  }
}
```



### Update Ruleset

`POST https://test-api.width.live/api/v3/rulesets/update`

Rename a ruleset, change its jurisdiction or icon, or replace a scoring ruleset's scoring_config. Send only what changes; omitted fields keep their values. Returns the updated ruleset. You see your own rulesets and the builtins; only your own can be changed — a builtin answers 403, clone it first. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_id` | number | yes | Ruleset id (must be your own) | 0 |
| `name` | string | no | New display name |  |
| `jurisdiction` | string | no | New jurisdiction |  |
| `scoring_config` | array (JSON) | no | Scoring rulesets only: replaces the whole config |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/update' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"ruleset_id":0,"scoring_config":[]}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 41,
    "name": "My KYA ruleset v2",
    "jurisdiction": "SG",
    "icon": "",
    "is_builtin": false,
    "generated_by": "api",
    "status": "ready",
    "ruleset_type": "kya",
    "source_policy_ids": [],
    "rules": [],
    "created_at": "2026-09-11T09:12:44Z",
    "updated_at": "2026-09-11T09:20:10Z"
  }
}
```



### Delete Ruleset

`POST https://test-api.width.live/api/v3/rulesets/delete`

Removes one of your rulesets and its rules. Snapshots that were screened with it keep their frozen copy of the hits, so history is unaffected; new screenings that still send its id fall back to the builtin default. Builtins cannot be deleted (403). Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_id` | number | yes | Ruleset id (must be your own) | 0 |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/delete' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"ruleset_id":0}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 41,
    "deleted": true
  }
}
```



### Clone Ruleset

`POST https://test-api.width.live/api/v3/rulesets/clone`

Copies a ruleset — a builtin or one of your own — into a new ruleset of yours, rules and highlights included. This is the normal way to start: clone the builtin AML KYA Default, then add, change or remove rules on the copy and pass the copy's id as ruleset_id when screening. Returns the new ruleset with its rules. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `source_ruleset_id` | number | yes | Ruleset to copy (builtin or your own) | 22 |
| `name` | string | no | Name for the copy; default appends "(Copy)" |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/clone' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"source_ruleset_id":22}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 42,
    "name": "AML KYA Default v1.3 (Copy)",
    "jurisdiction": "Global",
    "icon": "",
    "is_builtin": false,
    "generated_by": "kya-v2.1-hilite",
    "status": "ready",
    "ruleset_type": "kya",
    "source_policy_ids": [],
    "rules": [
      {
        "id": 3001,
        "ruleset_id": 42,
        "rule_code": "KYA_SANCTION_SELFHIT",
        "category": "Sanctions",
        "name": "Address is a sanctioned entity",
        "risk_level": "critical",
        "action": "block",
        "direction": "both",
        "min_hops": 0,
        "max_hops": -1,
        "conditions": [
          {
            "parameter": "primary_category",
            "operator": "==",
            "value": "Sanctions"
          }
        ],
        "sort_order": 1,
        "is_highlighted": true,
        "highlight_tags": []
      }
    ],
    "created_at": "2026-09-11T09:30:00Z",
    "updated_at": "2026-09-11T09:30:00Z"
  }
}
```



### List Rules

`POST https://test-api.width.live/api/v3/rulesets/rule/list`

The rules of one ruleset, in sort_order. Works for builtins too, so you can read the default catalogue before deciding what to change. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_id` | number | yes | Ruleset id | 22 |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/rule/list' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"ruleset_id":22}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": [
    {
      "id": 2101,
      "ruleset_id": 29,
      "rule_code": "KYA_SANCTION_SELFHIT",
      "category": "Sanctions",
      "name": "Address is a sanctioned entity",
      "description": "Address itself carries a Sanctions label.",
      "risk_level": "critical",
      "action": "block",
      "direction": "both",
      "min_hops": 0,
      "max_hops": -1,
      "reference": "",
      "conditions": [
        {
          "parameter": "primary_category",
          "operator": "==",
          "value": "Sanctions"
        }
      ],
      "sort_order": 1,
      "is_highlighted": true,
      "highlight_tags": [],
      "created_at": "2026-09-08T18:05:40Z",
      "updated_at": "2026-09-08T18:05:40Z"
    }
  ]
}
```



### Get Rule

`POST https://test-api.width.live/api/v3/rulesets/rule/get`

One rule. A rule fires when a fund-flow path (or, for max_hops=-1, the screened address itself) meets every condition: risk_level and action are what a hit carries into the result and the score; direction limits it to inbound (source of funds), outbound or both; min_hops / max_hops bound the path length. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `rule_id` | number | yes | Rule id | 0 |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/rule/get' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"rule_id":0}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 2140,
    "ruleset_id": 29,
    "rule_code": "KYA_SANCTION_EXPOSURE_H2",
    "category": "Sanctions",
    "name": "Exposure to sanctioned address (2 hops)",
    "description": "A sanctioned address two hops away, the path's smallest transfer at least 5% of its last.",
    "risk_level": "critical",
    "action": "block",
    "direction": "both",
    "min_hops": 2,
    "max_hops": 2,
    "reference": "OFAC",
    "conditions": [
      {
        "parameter": "primary_category",
        "operator": "==",
        "value": "Sanctions"
      },
      {
        "parameter": "risk_rate",
        "operator": ">=",
        "value": 5
      }
    ],
    "sort_order": 40,
    "is_highlighted": false,
    "highlight_tags": [],
    "created_at": "2026-09-08T18:05:40Z",
    "updated_at": "2026-09-10T19:30:02Z"
  }
}
```

**Response fields (data):**

- `rule_code` (string) — Your identifier, unique within the ruleset; it names the hit in results and snapshots
- `risk_level` (string) — low, medium, high or critical — drives the score's severity weight
- `action` (string) — alert, review, edd, block or allow — the disposition a hit recommends
- `direction` (string) — inbound, outbound or both
- `min_hops / max_hops` (number) — Path length the rule applies to; max_hops -1 = the address's own labels, no path
- `conditions[]` (array) — {parameter, operator, value} triples, all of which must hold. Every parameter, operator and legal value, with examples: Rule conditions
- `is_highlighted` (boolean) — Highlighted (★) rules are surfaced first in results; presentation only
- `highlight_tags` (string[]) — Scopes the ★: when non-empty, only hits whose counterparty label (any of its four category levels) contains one of these labels (case-insensitive) are highlighted. Empty = every hit of this rule is highlighted


### Create Rule

`POST https://test-api.width.live/api/v3/rulesets/rule/create`

Adds a rule to one of your rulesets. Required: rule_code (unique in the ruleset, 409 if taken), name, risk_level, action and at least one condition; direction defaults to both, min_hops / max_hops to 0 (max_hops -1 makes an identity rule that matches the screened address's own labels). Values are validated up front and a 400 names the field. The rule is live for the next screening that uses the ruleset. Returns the created rule. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_id` | number | yes | Your ruleset (not a builtin) | 0 |
| `rule_code` | string | yes | Unique code within the ruleset, e.g. MY_MIXER_EXPOSURE_H2 | MY_MIXER_EXPOSURE_H2 |
| `name` | string | yes | Rule name as shown in results | Exposure to a mixer (2 hops) |
| `category` | string | no | Risk category label, e.g. Obfuscation | Obfuscation |
| `description` | string | no | What the rule catches |  |
| `risk_level` | string (low \| medium \| high \| critical) | yes | low, medium, high or critical | high |
| `action` | string (alert \| review \| edd \| block \| allow) | yes | alert, review, edd, block or allow | review |
| `direction` | string (both \| inbound \| outbound) | no | inbound, outbound or both | inbound |
| `min_hops` | number | no | Shortest path length the rule applies to (0 = any) | 2 |
| `max_hops` | number | no | Longest path length; -1 = the address's own labels only | 2 |
| `conditions` | array (JSON) | yes | JSON array of {parameter, operator, value}; every condition must hold. Every parameter, operator and legal value, with examples: Rule conditions | [{"parameter":"primary_category","operator":"==","value":"Obfuscation"},{"parameter":"risk_rate","operator":">=","value":10}] |
| `is_highlighted` | boolean | no | Flag as a highlighted (★) rule | false |
| `highlight_tags` | array (JSON) | no | Scope the ★ to hits whose counterparty label contains one of these labels (case-insensitive). Omit or empty = every hit of this rule is highlighted |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/rule/create' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"ruleset_id":0,"rule_code":"MY_MIXER_EXPOSURE_H2","name":"Exposure to a mixer (2 hops)","category":"Obfuscation","risk_level":"high","action":"review","direction":"inbound","min_hops":2,"max_hops":2,"conditions":[{"parameter":"primary_category","operator":"==","value":"Obfuscation"},{"parameter":"risk_rate","operator":">=","value":10}],"is_highlighted":false,"highlight_tags":[]}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 3077,
    "ruleset_id": 42,
    "rule_code": "MY_MIXER_EXPOSURE_H2",
    "category": "Obfuscation",
    "name": "Exposure to a mixer (2 hops)",
    "description": "",
    "risk_level": "high",
    "action": "review",
    "direction": "inbound",
    "min_hops": 2,
    "max_hops": 2,
    "reference": "",
    "conditions": [
      {
        "parameter": "primary_category",
        "operator": "==",
        "value": "Obfuscation"
      },
      {
        "parameter": "risk_rate",
        "operator": ">=",
        "value": 10
      }
    ],
    "sort_order": 0,
    "is_highlighted": false,
    "highlight_tags": [],
    "created_at": "2026-09-11T09:41:07Z",
    "updated_at": "2026-09-11T09:41:07Z"
  }
}
```



### Update Rule

`POST https://test-api.width.live/api/v3/rulesets/rule/update`

Changes one of your rules. Send only the fields to change; conditions, when sent, replace the whole list. The same validation as create applies to whatever is sent; hops are checked against the stored counterpart. Returns the updated rule. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `rule_id` | number | yes | Rule id (in one of your rulesets) | 0 |
| `name` | string | no | New name |  |
| `risk_level` | string (low \| medium \| high \| critical) | no | low, medium, high or critical |  |
| `action` | string (alert \| review \| edd \| block \| allow) | no | alert, review, edd, block or allow |  |
| `direction` | string (both \| inbound \| outbound) | no | inbound, outbound or both |  |
| `conditions` | array (JSON) | no | Replacement condition list (JSON array). Every parameter, operator and legal value, with examples: Rule conditions |  |
| `is_highlighted` | boolean | no | Flag / unflag as highlighted | false |
| `highlight_tags` | array (JSON) | no | Replacement label scope for the ★ (case-insensitive counterparty-label match). Omit to keep the current scope, send [] to clear it back to unscoped |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/rule/update' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"rule_id":0,"conditions":[],"is_highlighted":false,"highlight_tags":[]}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 3077,
    "ruleset_id": 42,
    "rule_code": "MY_MIXER_EXPOSURE_H2",
    "category": "Obfuscation",
    "name": "Exposure to a mixer (2 hops)",
    "description": "",
    "risk_level": "critical",
    "action": "block",
    "direction": "inbound",
    "min_hops": 2,
    "max_hops": 2,
    "reference": "",
    "conditions": [
      {
        "parameter": "primary_category",
        "operator": "==",
        "value": "Obfuscation"
      },
      {
        "parameter": "risk_rate",
        "operator": ">=",
        "value": 10
      }
    ],
    "sort_order": 0,
    "is_highlighted": true,
    "highlight_tags": [
      "Obfuscation"
    ],
    "created_at": "2026-09-11T09:41:07Z",
    "updated_at": "2026-09-11T09:55:31Z"
  }
}
```



### Delete Rule

`POST https://test-api.width.live/api/v3/rulesets/rule/delete`

Removes a rule from one of your rulesets. Past snapshots keep the hits it produced. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `rule_id` | number | yes | Rule id (in one of your rulesets) | 0 |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/rule/delete' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"rule_id":0}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 3077,
    "deleted": true
  }
}
```



### Highlight Rules

`POST https://test-api.width.live/api/v3/rulesets/rule/highlight`

Flags or unflags several rules of one ruleset as highlighted (★) in one call. Highlighted rules are surfaced first in screening results and counted in highlightSummary; the flag never changes risk_level, action or the score. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). 

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `ruleset_id` | number | yes | Your ruleset | 0 |
| `rule_ids` | array (JSON) | yes | JSON array of rule ids in that ruleset | [] |
| `is_highlighted` | boolean | yes | true to flag, false to unflag | true |
| `highlight_tags` | array (JSON) | no | Replacement label scope applied to every rule in rule_ids. Omit = keep each rule's scope, [] = clear |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/rulesets/rule/highlight' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"ruleset_id":0,"rule_ids":[],"is_highlighted":true,"highlight_tags":[]}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "affected": 2
  }
}
```



## v3 Adverse Media

### Screen a Person

`POST https://test-api.width.live/api/v3/adverse-media/screen`

Screens a natural person for adverse media (negative news): sanctions, terrorism financing, money laundering, fraud and 8 other FATF-aligned categories. Not a sanctions-list check — "sanctions" here is a news category among the findings, not a match against an official list (OFAC / UN / EU). Always async: queues a job and returns immediately with a screening_id to poll via Get Screening Result; typical completion is well under a minute but can take a few minutes on a broad name. Every finding is AI-generated research, machine-scored and verified against its cited source — review the evidence before it informs a decision. full_name is the only required field; at least one of birth_date, nationality, residence_country, position, organization or x_handle ("a strong identifier") sharply reduces namesake false positives — without one, the result carries low_information:true and every finding is capped at match_level=medium. Same user + same normalised person within 7 days reuses the existing completed screening (cached:true, not charged) unless force_refresh is set. Auth: API key via the X-Api-Key header or ?apikey= (the same key as the screening endpoints). Rate limit: 60 requests/min per key (HTTP 429 beyond that); also subject to your daily screening limit and (once billing is enabled) token balance.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `full_name` | string | yes | Full name, any script (max 255 characters) |  |
| `aliases` | array (JSON) | no | Former names or other-script spellings (e.g. 中文/拼音/English), JSON array of strings, up to 20 | [] |
| `birth_date` | string | no | YYYY or YYYY-MM-DD — a strong identifier |  |
| `nationality` | string | no | ISO 3166-1 alpha-2 country code — a strong identifier |  |
| `residence_country` | string | no | ISO 3166-1 alpha-2 country code — a strong identifier |  |
| `position` | string | no | Job title / role — a strong identifier |  |
| `organization` | string | no | Employer or affiliated organization — a strong identifier |  |
| `gender` | string (male \| female \| other) | no | Not a strong identifier on its own (too coarse to corroborate or rule out identity) |  |
| `x_handle` | string | no | X (Twitter) handle, leading @ optional — a strong identifier |  |
| `customer_ref` | string | no | Your own record id for this person; echoed back and filterable in List Screenings |  |
| `force_refresh` | boolean | no | Bypass the 7-day cache and run a fresh, charged screening even if an identical one completed recently | false |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/adverse-media/screen' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"full_name":"","aliases":[],"force_refresh":false}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "screening_id": 4521,
    "subject_id": 812,
    "status": "queued",
    "cached": false
  }
}
```

**Response fields (data):**

- `screening_id` (number) — Poll GET /api/v3/adverse-media/result/{screening_id} with this id
- `subject_id` (number) — The (possibly reused) subject profile this screening belongs to
- `status` (string) — queued | searching | verifying | completed | failed — already completed when cached
- `cached` (boolean) — true when an identical screening for this person completed within the last 7 days and was reused instead of a fresh, charged run


### Get Screening Result

`GET https://test-api.width.live/api/v3/adverse-media/result/{screeningId}`

Poll a screening job by id. Status moves queued → searching → verifying → completed (or failed); poll no faster than every few seconds. Findings are sorted worst/most-actionable first; each carries its category, event status, tri-lingual summary, match level with matched/conflicting attribute evidence, every source with its verification outcome, and a score. counted is the effective decision after any analyst disposition (confirmed / false_positive / irrelevant); machine_counted is the pipeline's own decision before review. negative_press findings (negative news that alleges no offence) are returned for review with informational:true and never counted. review_required:true (edd/block verdict) means every counted finding must be dispositioned before the screening can be closed. Auth: API key via the X-Api-Key header or ?apikey=. Nothing is charged by polling.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `screeningId` | number | yes | The screening_id from Screen a Person (path parameter) |  |

**Example request:**

```bash
curl 'https://test-api.width.live/api/v3/adverse-media/result/<screeningId>' -H 'X-Api-Key: $DART_API_KEY'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "id": 4521,
    "subject_id": 812,
    "status": "completed",
    "source": "api_v3",
    "input": {
      "full_name": "Wei Zhang",
      "aliases": [],
      "birth_date": "1979",
      "nationality": "CN",
      "residence_country": "",
      "position": "Director",
      "organization": "Acme Holdings",
      "gender": "",
      "x_handle": "",
      "customer_ref": "CUST-001"
    },
    "score": 82,
    "verdict": "block",
    "original_score": 82,
    "original_verdict": "block",
    "review_required": true,
    "low_information": false,
    "ai_generated": true,
    "closed": false,
    "cached": false,
    "model": "grok-4.7",
    "findings": [
      {
        "id": 9001,
        "category": "sanctions",
        "event_status": "charged",
        "event_date": "2025-03-10",
        "title_original": "Executive charged over sanctions evasion scheme",
        "summary": {
          "en": "Charged with evading export controls via shell entities.",
          "zh": "被指控通过空壳实体规避出口管制。",
          "zh_tw": "被指控透過空殼實體規避出口管制。"
        },
        "name_match": "full",
        "match_level": "high",
        "matched_attributes": [
          {
            "attribute": "organization",
            "value": "Acme Holdings",
            "quote": "Acme Holdings director Wei Zhang was charged..."
          }
        ],
        "conflicting_attributes": [],
        "sources": [
          {
            "url": "https://news.example.com/article",
            "title": "Executive charged over sanctions evasion",
            "publisher": "Example News",
            "published_date": "2025-03-10",
            "kind": "web",
            "name_in_source": "Wei Zhang",
            "keywords_in_source": [
              "sanctions",
              "charged"
            ],
            "cited": true,
            "verified": true,
            "verify_reason": "ok",
            "excerpt": "...Wei Zhang, director of Acme Holdings, was charged with..."
          }
        ],
        "source_kind": "web",
        "web_verified": true,
        "x_verified": false,
        "score": 82,
        "counted": true,
        "machine_counted": true,
        "informational": false,
        "disposition": "",
        "disposition_note": ""
      }
    ],
    "counts": {
      "total": 1,
      "counted": 1,
      "informational": 0,
      "unverified": 0,
      "low_match": 0,
      "dismissed": 0
    },
    "identity_notes": "Single individual matched consistently across sources.",
    "searched_queries": [
      "Wei Zhang Acme Holdings sanctions",
      "张伟 制裁"
    ],
    "created_at": "2026-09-24T08:12:00Z",
    "started_at": "2026-09-24T08:12:01Z",
    "completed_at": "2026-09-24T08:12:47Z"
  }
}
```

**Response fields (data):**

- `status` (string) — queued | searching | verifying | completed | failed
- `input` (object) — The submitted subject fields, normalised
- `score` (number) — 0–100, present once completed. Bands (platform defaults): review 20 / edd 50 / block 80
- `verdict` (string) — accept | review | edd | block
- `original_score` (number) — Score before any analyst disposition — kept for audit once dispositions change the live score/verdict
- `original_verdict` (string) — Verdict before any analyst disposition
- `review_required` (boolean) — true for edd/block: every counted finding must be dispositioned before Close Screening is allowed
- `low_information` (boolean) — true when no strong identifier was given — namesake risk is high and every finding is capped at match_level medium
- `ai_generated` (boolean) — Always true — every finding is AI-generated research requiring human review
- `closed` (boolean) — Screenings are closed explicitly (see the v1 console flow); v3 currently reads and lists, closing is done from the console
- `cached` (boolean) — Always false here — only Screen a Person's own response reports a cache hit
- `findings` (object[]) — Sorted worst/most-actionable first
  - `[].category` (string) — One of 13 fixed categories: sanctions, terrorism, money_laundering, fraud, cybercrime, trafficking, organized_crime, corruption, securities, tax_crime, regulatory, other_crime, negative_press. negative_press is negative news coverage that alleges none of the other offences (workplace or product-safety scandals, tax or business disputes, lawsuits, public controversies) — reported for review but never scored
  - `[].event_status` (string) — reported | investigation | charged | convicted | settled | cleared
  - `[].summary` (object) — Tri-lingual summary: .en, .zh, .zh_tw
  - `[].match_level` (string) — high | medium | low — how confidently this event is the same person. low findings never count
  - `[].matched_attributes` (object[]) — Corroborating evidence: {attribute, value, quote} — quote is verbatim text the source is claimed to contain
  - `[].conflicting_attributes` (object[]) — Contradicting evidence, same shape
  - `[].sources` (object[]) — Every source is fetched server-side and checked; a fabricated citation fails verification
    - `[].verified` (boolean) — The fetched page contained the claimed name and at least one claimed keyword
    - `[].verify_reason` (string) — ok when verified; otherwise name_not_found, keyword_not_found, empty_page, no_claim, timeout, blocked_address, unsupported_scheme, fetch_error, or http_<status> (e.g. http_404)
    - `[].cited` (boolean) — false means the model named this URL but it was not among the search tools' own citations for this response — a fabrication warning sign
    - `[].kind` (string) — web or x (X/Twitter, verified via the public oEmbed endpoint)
  - `[].score` (number) — severity(category) × status × recency × match × source — see the Product Manual for the full table
  - `[].counted` (boolean) — Effective decision: machine_counted with any analyst dismissal (false_positive/irrelevant) applied
  - `[].machine_counted` (boolean) — The pipeline's own decision before any review: match ≠ low, at least one source verified, and a scored category (never true for negative_press)
  - `[].informational` (boolean) — true for negative_press: shown for review, never counted toward score or verdict
  - `[].disposition` (string) — confirmed | false_positive | irrelevant | "" (not yet reviewed)
- `counts` (object) — total / counted / informational / unverified / low_match / dismissed finding counts. informational = negative_press findings that are verified, not low match and not dismissed
- `searched_queries` (string[]) — Queries actually run across web and X search


### List Screenings

`POST https://test-api.width.live/api/v3/adverse-media/list`

Your screenings, most recent first, paginated. Filter by verdict, job status, or your own customer_ref. Findings are omitted from each item (fetch Get Screening Result for the full findings list) — this endpoint is for finding the right screening_id, not for re-reading every finding. Auth: API key via the X-Api-Key header or ?apikey=. Nothing is charged.

**Request parameters:**

| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| `page` | number | no | 1-based page number | 1 |
| `page_size` | number | no | Max 100 | 20 |
| `verdict` | string (accept \| review \| edd \| block) | no | Keep one verdict band. Empty = all |  |
| `status` | string (queued \| searching \| verifying \| completed \| failed) | no | Keep one job status. Empty = all |  |
| `customer_ref` | string | no | Exact match on your own record id |  |

**Example request:**

```bash
curl -X POST 'https://test-api.width.live/api/v3/adverse-media/list' \
  -H 'X-Api-Key: $DART_API_KEY' -H 'Content-Type: application/json' \
  -d '{"page":1,"page_size":20}'
```

**Example response:**

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "items": [
      {
        "id": 4521,
        "subject_id": 812,
        "status": "completed",
        "source": "api_v3",
        "score": 82,
        "verdict": "block",
        "review_required": true,
        "low_information": false,
        "ai_generated": true,
        "closed": false,
        "cached": false,
        "model": "grok-4.7",
        "findings": [],
        "counts": {
          "total": 1,
          "counted": 1,
          "informational": 0,
          "unverified": 0,
          "low_match": 0,
          "dismissed": 0
        },
        "searched_queries": [],
        "created_at": "2026-09-24T08:12:00Z",
        "completed_at": "2026-09-24T08:12:47Z"
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20
  }
}
```

**Response fields (data):**

- `items[].findings` (object[]) — Always empty here — use Get Screening Result for the full list
- `items[].counts` (object) — total / counted / informational / unverified / low_match / dismissed — still populated even though findings is empty
- `total` (number) — Total matching screenings across all pages

