# adverse-media

Source: https://docs.korala.ai/guides/adverse-media

---

# Adverse media

Korala searches the news for a person or a company and reads each article it
finds. For each one it decides whether the article is about your subject,
whether it reports wrongdoing, what kind, and how far the matter had gone.
You get the articles that may be adverse, each with its link, its date, a
short quote and the reason. A reviewer then decides.

Adverse media is for enhanced due diligence. Run it when your risk policy
calls for it (a PEP, a high-risk country, a large amount) or when a reviewer
asks. It is not meant for every customer. A finding never declines anyone on
its own: it goes to review with its citation.

## How it works

1. **Search.** Korala sends up to four searches: each name alone, and each
   name with words for fraud, bribery, laundering, conviction and charges in
   that name's language. The language comes from the name's script (Hangul,
   kana) or from the subject's country. Terms exist in English, Czech,
   French, German, Japanese, Korean and Spanish; other countries get the
   English terms. A search returns at most ten results.
2. **De-duplicate.** Different addresses for one article count once: the
   mobile and AMP editions, `www.`, and tracking parameters are ignored.
3. **Judge.** Korala's model reads at most 40 articles: each title and a
   short excerpt, with the name, country and birth year you gave. Nothing
   else about your subject is sent.
4. **Decide.** An article judged adverse and about your subject is a
   **finding**. An article judged adverse where it is unclear whether it is
   about your subject is also a finding, marked `aboutSubject: "unclear"`.
   Articles about someone else, or with nothing adverse in them, are not
   kept.

Articles are not filtered by date. Each finding shows its publication date
so the reviewer can weigh an old matter against a recent one.

Korala keeps the link, the title, the publication date, a quote of at most
280 characters, the model's reason, and which search provider, model and
prompt version judged it. It never keeps the article. It does not keep the
name you searched either: the run holds a keyed digest of it.

## Run a search

```bash
POST /api/v1/screening/adverse-media/runs
Idempotency-Key: edd-2041-media
```

```json
{
  "subject": {
    "name": "Ada Finding Testperson",
    "country": "US",
    "birthYear": 1971
  },
  "riskSubjectId": "4c6f1b2a-8f0e-4d7a-9b3c-2e5d1a0f7c88"
}
```

| Field | Notes |
| --- | --- |
| `subject.name` | Required, up to 120 characters, in any script. |
| `subject.aliases` | At most one other name, typically the name in its own script. |
| `subject.entityType` | `person` (the default) or `entity`. |
| `subject.country` | ISO 3166-1 alpha-2 or alpha-3. Chooses the search language and helps rule out namesakes. |
| `subject.birthYear` | Year of birth or founding. Only the model sees it. |
| `subject.languages` | Up to two of `en`, `cs`, `de`, `es`, `fr`, `ja`, `ko`, instead of the language Korala would choose. |
| `riskSubjectId` | Optional: a risk subject in the same environment to record the outcome on. |

`Idempotency-Key` is required. Retry with the same key and body to get the
same run; a different body with a used key returns `409`. A run takes a few
seconds and at most 75 seconds, and the response is the finished run.

```json
{
  "id": "9a2e4c71-5b3d-4f08-a6e1-7c0d2b9f3e15",
  "status": "finding",
  "incomplete": false,
  "incompleteReasons": [],
  "errorCode": null,
  "sandbox": true,
  "simulated": true,
  "retrieval": {
    "provider": "fake",
    "queries": [
      { "kind": "name", "nameIndex": 0, "language": "en", "results": 1, "failure": null },
      { "kind": "adverse", "nameIndex": 0, "language": "en", "results": 2, "failure": null }
    ],
    "articlesRetrieved": 3,
    "articlesDropped": 0,
    "articlesUnique": 2
  },
  "judge": {
    "provider": "fake",
    "model": "fake-judge",
    "promptVersion": "adverse-media-judge-v3",
    "rulesVersion": "rules-v1",
    "articlesJudged": 2,
    "articlesUnjudged": 0
  },
  "riskFact": "screening.adverse_media",
  "riskSource": "korala:screening:adverse_media:v1",
  "findingCount": 1,
  "findings": [
    {
      "id": "e1c8b0d2-7a4f-4e39-8c15-0b6d2f9a3e71",
      "url": "https://news.example/adverse-media/finding/report",
      "title": "Sandbox report article",
      "publishedAt": "2025-06-15",
      "snippet": "A simulated report for the sandbox scenario.",
      "aboutSubject": "yes",
      "category": "fraud",
      "stage": "conviction",
      "reason": "Sandbox: a simulated report of a fraud conviction.",
      "foundBy": ["adverse:0:en"],
      "retrievalProvider": "fake",
      "judgeModel": "fake-judge",
      "promptVersion": "adverse-media-judge-v3",
      "decision": "pending"
    }
  ]
}
```

### Limits

An organization may start 30 runs a minute and 300 an hour in each mode,
whichever key or dashboard user starts them, and may have 2 live runs in
progress at once. Beyond any of these the request answers `429` with a `Retry-After`
header in seconds. Test mode and live mode
are counted apart, so test runs never hold up live ones, and test runs
never count toward the runs in progress.

A retry with the same `Idempotency-Key` while the first request is still
running waits for it and returns the same run. Only one retry waits at a
time: another one answers `409` with `"error": "request_in_progress"` and a
`Retry-After` header, so retry it with the same key once the first has
finished. A `409` without that code is permanent: the key was used for a
different request.

`GET /api/v1/screening/adverse-media/runs/{id}` returns a run with its
findings. `GET /api/v1/screening/adverse-media/runs` lists runs, newest
first, filtered by `status`, `sessionId` or `riskSubjectId`, with `page` and
`limit` (at most 100).

### Categories and stages

`category` is one of `fraud`, `bribery_corruption`, `money_laundering`,
`market_abuse`, `tax_fraud`, `sanctions_evasion`, `terrorism_financing` and
`other_crime`. `stage` is `allegation`, `investigation`, `charge`,
`conviction` or `sanction`, or `null` when the article does not say.

### Outcomes

| `status` | Meaning |
| --- | --- |
| `finding` | A finding about your subject is open, or a reviewer confirmed one. |
| `possible_finding` | Findings are open, and for each one it is unclear whether it is about your subject. |
| `clear` | Nothing adverse was found, and every search and article was completed. |
| `incomplete` | Nothing adverse was found, but something was not searched or not judged. This is not a clear result. |
| `error` | Nothing could be searched or judged. See `errorCode`. |

`incomplete` is also a flag. A finding from a partial run keeps its status
and has `incomplete: true`. `incompleteReasons` says why: `query_failed`,
`names_capped`, `queries_capped`, `articles_capped`, `judge_failed`,
`judge_output_invalid` or `deadline_exceeded`. If the model's answer cannot
be read, those articles count as not judged. They never count as clear.

## Review a finding

An organization owner or admin, signed in to the dashboard, confirms a
finding or marks it a false positive, with a reason:

```bash
POST /api/v1/screening/adverse-media/findings/{id}/decision
Idempotency-Key: edd-2041-review-1
```

```json
{ "decision": "false_positive", "reason": "Different person: the article names a surgeon in Denver" }
```

The run's status is worked out again from its findings. A false positive no
longer counts. A confirmed finding makes the run a `finding`. Every decision
is kept in an audit trail. API keys and members get `403`.

## Risk evidence

When a run names a risk subject, or runs in a session that has one, its
outcome is recorded as the fact `screening.adverse_media` from the source
`korala:screening:adverse_media:v1`. The value is `finding`,
`possible_finding` or `clear`. An `incomplete` or `error` run records the
fact as unavailable, so your policy treats it as missing evidence. Trust the
source in a policy input of type `string` for that fact only, and write
conditions on those three values. A decision records new evidence only while
that run is still the subject's newest adverse-media result.

## In a workflow

Add a check step with the `korala_adverse_media` provider:

```json
{
  "type": "check",
  "config": {
    "kind": "adverse_media",
    "provider": "korala_adverse_media",
    "providerConfig": { "languages": ["ko"] }
  }
}
```

Workflow steps always run: a step cannot be made conditional. To search
only on request, put the step in a workflow that only your enhanced due
diligence session configuration uses, and start that configuration only
for the customers your policy names (a PEP, a high-risk country, a large
amount). Otherwise call the API when a reviewer asks.

The step searches for the name on the session's identity document, the
name printed in the document's own script, the issuing country and the year
of birth. `providerConfig` takes only `languages` (optional, up to two). A
finding, a possible finding or an incomplete run sends the session to
review. An error is recorded for review. That includes `concurrency_limited`: when
the organization already has 2 live runs in progress, the step waits for
one to finish, asking again every few seconds for up to a minute, then
records an error rather than a result. The step is
never a `match`, and a
workflow with `blockingMode: "block"` on this step is refused when you save
it.

## Test mode

Test mode never contacts a search provider or a model. Its runs are
`simulated: true`, with articles at `news.example`. A word in the subject's
name picks what happens:

| Word in the name | Outcome |
| --- | --- |
| `Finding` | `finding`: a fraud conviction about the subject |
| `Possible` | `possible_finding`: a bribery allegation, subject unclear |
| `Namesake` | `clear`: the adverse article is about someone else, so it is not kept |
| `Partial` | `incomplete`: the adverse-terms search fails |
| `Malformed` | `error` (`judge_failed`): the model's answer cannot be read |
| `Unavailable` | `error` (`retrieval_failed`): every search fails |
| none of these | `clear` |

Live mode needs adverse media to be configured on Korala's side. Until it
is, live requests answer `503` and a live workflow step records an `error`.
A live run is never `clear` without a search.

## Webhooks

`screening.adverse_media.run.completed` carries the run id, status,
`incomplete`, its reasons, the error code, trigger, `simulated`, the session
and risk subject ids and the finding count.
`screening.adverse_media.finding.found` carries each finding's id,
category, stage and `aboutSubject`. Neither includes the name, a link or a
quote: read the run for those. Delivery is at least once. A decision sends
no event.

## Data protection

Findings are your evidence about your customer. They are kept under your
retention, in your organization and environment only. Korala keeps no list
of people linked to offences and does not share findings across
organizations.
