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

# Migration Guide: Legacy → v2 API

> Step-by-step guide to migrate your compliance screening integration from the legacy KYC endpoint to the current v2 API.

This guide helps you migrate an existing integration from the **legacy KYC screening
endpoint** to the **current v2 API**. The v2 API keeps the same screening engine and
list coverage, but standardizes the request/response envelope, authentication, and
identifier matching so integrations are simpler and more predictable.

<Info>
  The screening logic, watchlists, and matching quality are the same across both
  versions. Migration is almost entirely about **how you authenticate** and **how you
  read the response** — the request body is largely backward compatible.
</Info>

## Why migrate

<CardGroup cols={2}>
  <Card title="Standard response envelope" icon="box">
    Every endpoint now returns a consistent `status` + payload + `execution_context`
    structure, so error handling and parsing are uniform across the platform.
  </Card>

  <Card title="Unified authentication" icon="key">
    A single `Authorization: Bearer` scheme (API key or JWT) replaces the legacy
    per-endpoint auth, with role-based permissions.
  </Card>

  <Card title="Value-first identifier matching" icon="fingerprint">
    Document and ID matching now keys on the **value**, not the document-type label —
    you no longer need to send the exact `document_type` to get an exact ID hit.
  </Card>

  <Card title="Multi-list in one call" icon="layer-group">
    Screen against many lists in a single request with a consolidated `summary` and
    per-list `results`.
  </Card>
</CardGroup>

## At a glance

| Aspect              | Legacy                                              | v2                                                                               |
| ------------------- | --------------------------------------------------- | -------------------------------------------------------------------------------- |
| Base URL            | Legacy gateway host                                 | `https://kyc.legaltalent.ai` (prod) · `https://stg.kyc.legaltalent.ai` (staging) |
| Auth header         | Legacy credential                                   | `Authorization: Bearer <api_key \| jwt>`                                         |
| Endpoint            | Legacy screening path                               | `POST /kyc`                                                                      |
| Response envelope   | Job object at the top level (`job_id`, `status`, …) | `{ "status", "result" \| "results" + "summary", "execution_context" }`           |
| Request identifier  | `job_id` in body                                    | `execution_context.request_id` in response                                       |
| Top-level status    | `"COMPLETED"` / `"FAILED"`                          | `"success"` / `"error"`                                                          |
| Per-list match flag | `has_matches`                                       | `is_match`                                                                       |
| Matched entity      | `matches[].matched_entity`                          | `matches[].match_data`                                                           |
| Per-match risk      | `matches[].risk_level` + `confidence_score`         | `matches[].confidence_score` (derive level from score)                           |
| Aggregate summary   | `screening_summary`                                 | `summary` (multi-list only)                                                      |

## Migration in 5 steps

<Steps>
  <Step title="Switch the base URL and endpoint">
    Point your client at `https://stg.kyc.legaltalent.ai/kyc` for integration testing,
    then `https://kyc.legaltalent.ai/kyc` for production. The method stays `POST`.
  </Step>

  <Step title="Switch to Bearer authentication">
    Replace the legacy credential with an `Authorization: Bearer` header using your
    API key (`sk_...`) or a JWT. See [Authentication](/api-reference/auth).
    You no longer pass `tenant_id` in the body — it is derived from the token.
  </Step>

  <Step title="Keep your request body (with minor tweaks)">
    The `subject` object is backward compatible. If you used `match_type`, send
    `search_type` instead. Accepted values: `exact`, `fuzzy`, `token`, `composite`,
    `fast_precise_cascade` (default). The legacy default was `composite` — send it
    explicitly if you want to preserve the previous matching behavior. Lists move
    from `lists_to_check` to `lists`.
  </Step>

  <Step title="Update your response parser">
    Read results from the new envelope: `result` (single list) or `results` keyed by
    list name plus `summary` (multi-list), and `execution_context.request_id` for the
    correlation id. There is no `data` or `meta` wrapper. See the field mapping below.
  </Step>

  <Step title="Re-point your risk/decision logic">
    The v2 response does **not** return an aggregate risk level or recommended action.
    Use `matches[].confidence_score` for per-match intensity and `is_match` /
    `summary.match_count` for the decision, deriving your own risk level from the score
    (see [Risk and decisioning](#risk-and-decisioning)).
  </Step>
</Steps>

## Authentication

The legacy integration used a dedicated credential on the legacy gateway. The v2 API
uses a single Bearer scheme for both user (JWT) and machine-to-machine (API key)
access.

<CodeGroup>
  ```bash v2 (API key) theme={null}
  curl -X POST https://kyc.legaltalent.ai/kyc \
    -H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{ "subject": { "full_name": "John Doe" }, "list_name": "ofac" }'
  ```

  ```bash v2 (JWT) theme={null}
  curl -X POST https://kyc.legaltalent.ai/kyc \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
    -H "Content-Type: application/json" \
    -d '{ "subject": { "full_name": "John Doe" }, "list_name": "ofac" }'
  ```
</CodeGroup>

<Note>
  `POST /kyc` requires the `kyc:create` permission. The token also scopes the request
  to your tenant, so **remove any `tenant_id` you previously sent in the request body**.
</Note>

## Request changes

The request body is largely unchanged. The main adjustments:

| Legacy field       | v2 field      | Notes                                                                                                                                                                |
| ------------------ | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `match_type`       | `search_type` | `search_type` is the canonical name. The default changed from `composite` to `fast_precise_cascade` — send `"search_type": "composite"` to keep the legacy behavior. |
| `lists_to_check`   | `lists`       | Renamed. Omit it (or send an empty array) to check every enabled list.                                                                                               |
| `tenant_id` (body) | —             | Removed. Derived from the Bearer token.                                                                                                                              |
| `subject.*`        | `subject.*`   | Backward compatible (see below).                                                                                                                                     |

### Identifier matching improvement

In v2, identifier matching is **value-first**: an exact ID/document number matches a
list entry regardless of how the document-type label is named on either side
(`CI`, `DNI`, `National ID`, `document_id`, numeric `id`, etc.). Values are normalized
(non-alphanumeric characters stripped, case-insensitive) before comparison.

<Tabs>
  <Tab title="Recommended (v2)">
    You no longer need `document_type` to get an exact ID hit:

    ```json theme={null}
    {
      "subject": {
        "full_name": "Jane Roe",
        "document_id": "30708422459"
      },
      "list_name": "senaclaft_uy"
    }
    ```

    This matches a list entry whether the number is stored under `ci`, `document_id`,
    a numeric `id`, or inside an `identifiers[]` array.
  </Tab>

  <Tab title="Still supported">
    Sending `document_type` (or `identifiers[]`) continues to work and does not block a
    match:

    ```json theme={null}
    {
      "subject": {
        "full_name": "Jane Roe",
        "document_id": "30.708.422-459",
        "document_type": "CI",
        "nationality": "UY"
      },
      "list_name": "senaclaft_uy"
    }
    ```
  </Tab>
</Tabs>

<Warning>
  Country (`nationality`) and `birth_date` behave the same as before: they refine
  **name-based** matches (and can lower confidence on a mismatch). They never affect an
  **exact identifier** match, which always returns `confidence_score: 1.0`.
</Warning>

## Response changes

The biggest change is the envelope. Below is the same multi-list screening in both
formats, side by side.

<CodeGroup>
  ```json Legacy response theme={null}
  {
    "job_id": "abc-123-def",
    "status": "COMPLETED",
    "screened_entity": {
      "name": "John Doe",
      "entity_type": "individual"
    },
    "results": {
      "ofac": {
        "list_name": "ofac",
        "list_display_name": "OFAC Sanctions List",
        "status": "CHECKED",
        "has_matches": true,
        "match_count": 1,
        "highest_risk_level": "HIGH",
        "matches": [
          {
            "match_id": "m-1",
            "match_type": "FUZZY",
            "confidence_score": 0.95,
            "risk_level": "HIGH",
            "matched_field": "name",
            "matched_entity": {
              "entity_id": "12345",
              "name": "JOHN DOE",
              "entity_type": "individual",
              "aliases": ["Johnny Doe"],
              "nationality": "US"
            }
          }
        ]
      }
    },
    "screening_summary": {
      "total_lists_checked": 1,
      "total_matches": 1,
      "lists_with_matches": ["ofac"],
      "overall_risk_level": "HIGH",
      "recommended_action": "REVIEW - Manual review required before proceeding.",
      "requires_manual_review": true
    }
  }
  ```

  ```json v2 response theme={null}
  {
    "status": "success",
    "results": {
      "ofac": {
        "list_name": "ofac",
        "subject": { "full_name": "John Doe", "nationality": "US" },
        "is_match": true,
        "matches": [
          {
            "confidence_score": 0.95,
            "match_type": "composite",
            "match_timestamp": "2026-06-29T10:30:00Z",
            "source_list": "ofac",
            "match_data": {
              "name": "JOHN DOE",
              "entity_type": "individual",
              "source": "ofac",
              "sanction_programs": ["SDGT"],
              "nationality": "US"
            }
          }
        ],
        "match_count": 1,
        "check_timestamp": "2026-06-29T10:30:00Z",
        "processing_time_ms": 800,
        "metadata": { "search_type": "fast_precise_cascade" },
        "execution_time_ms": 805
      }
    },
    "summary": {
      "is_match": true,
      "match_count": 1,
      "lists_checked": ["ofac"],
      "lists_with_matches": ["ofac"],
      "has_errors": false,
      "error_count": 0,
      "execution_time_ms": 3200,
      "timestamp": "2026-06-29T10:30:00Z"
    },
    "execution_context": {
      "request_id": "abc-123-def",
      "processing_time_ms": 3200,
      "timestamp": "2026-06-29T10:30:00Z",
      "tenant_id": "tnt-123"
    }
  }
  ```
</CodeGroup>

### Field mapping

| Legacy path                                | v2 path                                           |
| ------------------------------------------ | ------------------------------------------------- |
| `status == "COMPLETED"`                    | `status == "success"`                             |
| `job_id`                                   | `execution_context.request_id`                    |
| `completed_at`                             | `execution_context.timestamp`                     |
| `screened_entity.name`                     | `results[list].subject.full_name`                 |
| `results[list].has_matches`                | `results[list].is_match`                          |
| `results[list].match_count`                | `results[list].match_count`                       |
| `results[list].matches[].matched_entity`   | `results[list].matches[].match_data`              |
| `results[list].matches[].confidence_score` | `results[list].matches[].confidence_score`        |
| `results[list].matches[].match_type`       | `results[list].matches[].match_type`              |
| `results[list].matches[].risk_level`       | *(derive from `confidence_score`)*                |
| `screening_summary`                        | `summary`                                         |
| `screening_summary.total_matches`          | `summary.match_count`                             |
| `screening_summary.total_lists_checked`    | `summary.lists_checked` (array — use its length)  |
| `screening_summary.overall_risk_level`     | *(not returned — derive from `confidence_score`)* |
| `screening_summary.recommended_action`     | *(not returned — apply your own decision policy)* |

<Note>
  **Single-list** responses (using `list_name`) return the result under a top-level
  `result` object and have **no** `summary`. **Multi-list** responses (using `lists`,
  or omitting both) return `results` keyed by list name plus an aggregate `summary`.
  In multi-list responses each match also carries a `source_list` field. There is no
  `data` or `meta` wrapper. See [List Check](/api-reference/list-check) for full schemas.
</Note>

## Risk and decisioning

The v2 API returns the **raw matching signal** and leaves the risk classification to
you. There is **no** `overall_risk_level`, `recommended_action`, or per-match
`risk_level` in the response.

* **Per match — `confidence_score` (0.0–1.0):** the intensity of an individual hit.
  An exact identifier match is always `1.0`. Name matches are scored by the search
  engine and refined by `nationality` / `birth_date`.
* **Aggregate — `summary.is_match` / `summary.match_count` / `summary.lists_with_matches`:**
  whether anything matched, how many, and on which lists.

If your legacy integration read **per-match `risk_level`** or an aggregate
`overall_risk_level`, derive your own level from `confidence_score` using thresholds
that fit your risk appetite:

```python theme={null}
def risk_level_from_score(score: float) -> str:
    if score >= 0.75:
        return "CRITICAL"   # exact / very strong match
    if score >= 0.30:
        return "HIGH"
    if score >= 0.15:
        return "MEDIUM"
    return "LOW"
```

<Info>
  The thresholds above mirror the platform defaults, but you own the final decision
  policy (block / review / proceed). Tune the cutoffs to your compliance requirements.
</Info>

## Error handling

Errors also move into the standard envelope.

<CodeGroup>
  ```json Legacy error theme={null}
  {
    "status": "FAILED",
    "error_message": "Subject must have at least one identifier or name"
  }
  ```

  ```json v2 error theme={null}
  {
    "status": "error",
    "error": {
      "type": "ValidationError",
      "message": "Subject must have at least one of: full_name, document_id, wallet_address, identifiers, or imo_number",
      "code": "VALIDATION_ERROR"
    },
    "execution_context": {
      "request_id": "abc-123-def",
      "processing_time_ms": 5,
      "timestamp": "2026-06-29T10:30:00Z"
    }
  }
  ```
</CodeGroup>

| Situation      | Legacy                   | v2                                                              |
| -------------- | ------------------------ | --------------------------------------------------------------- |
| Success flag   | `status == "COMPLETED"`  | `status == "success"`                                           |
| Failure flag   | `status == "FAILED"`     | `status == "error"`                                             |
| Error detail   | `error_message` (string) | `error.type` + `error.message` (+ optional `error.code`)        |
| Correlation id | `job_id`                 | `execution_context.request_id`                                  |
| Auth failure   | gateway error            | `401` (missing/invalid token) / `403` (insufficient permission) |

See [List Check → Error Responses](/api-reference/list-check) and
[Authentication → Error Responses](/api-reference/auth) for the full catalog.

## Migration checklist

<Steps>
  <Step title="Update endpoint and base URL">
    `POST /kyc` on the staging host first.
  </Step>

  <Step title="Move to Bearer auth">
    Use an API key or JWT; drop the legacy credential and any body-level `tenant_id`.
  </Step>

  <Step title="Rename match_type → search_type and lists_to_check → lists">
    Send `"search_type": "composite"` to keep the legacy default behavior.
  </Step>

  <Step title="Re-map the response">
    Read `result` (single) or `results` + `summary` (multi); switch
    `has_matches → is_match`, `matched_entity → match_data`,
    `job_id → execution_context.request_id`. Drop any `data` / `meta` access.
  </Step>

  <Step title="Re-point risk logic">
    Derive your own risk level from `matches[].confidence_score`; the v2 response has no
    `overall_risk_level` / `recommended_action`.
  </Step>

  <Step title="Update error handling">
    Branch on `status` and read `error.type` / `error.message`; handle `401` / `403`.
  </Step>

  <Step title="Validate on staging, then cut over">
    Run representative cases against staging, compare to legacy output, then switch the
    production base URL.
  </Step>
</Steps>

## FAQ

<AccordionGroup>
  <Accordion title="Do I have to change my request body?">
    Minimally. The `subject` object is backward compatible. The only common changes are
    renaming `match_type` to `search_type` and removing any `tenant_id` from the body.
  </Accordion>

  <Accordion title="Will the same input produce the same matches?">
    Yes — the screening engine and lists are the same. Identifier matching is more
    forgiving (value-first), so you may see exact ID hits that previously required the
    correct `document_type`. Name-based scoring is unchanged.
  </Accordion>

  <Accordion title="What happened to per-match risk_level?">
    The v2 response does not return a risk level — neither per match nor aggregate.
    Each match exposes `confidence_score` (0.0–1.0); derive your own level from it and
    apply your decision policy. Use `summary.is_match` / `summary.match_count` for the
    overall outcome.
  </Accordion>

  <Accordion title="Can I run both versions during migration?">
    Yes. Point a copy of your traffic at the v2 staging endpoint, compare outputs, and
    cut over once parity is confirmed. There is no forced switch on the request side.
  </Accordion>

  <Accordion title="Do I need to re-issue credentials?">
    You need a v2 API key (or JWT) for Bearer auth. Contact your account administrator
    to provision an API key with the `kyc:create` permission.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="List Check reference" icon="list-check" href="/api-reference/list-check">
    Full v2 request/response schemas, lists, and examples.
  </Card>

  <Card title="Authentication" icon="key" href="/api-reference/auth">
    Bearer tokens, API keys, and the permission model.
  </Card>

  <Card title="API Overview" icon="book" href="/api-reference/introduction">
    Environments, rate limits, and platform basics.
  </Card>

  <Card title="Validate a Person or Entity" icon="user-check" href="/validate-person-entity">
    End-to-end screening walkthrough on v2.
  </Card>
</CardGroup>
