# CustosHUB API — Quickstart

Screen individuals and entities against OFAC **SDN** and **Consolidated** lists with weighted multi-field matching and field-level explain.

- **Base URL:** `https://api.example.com` (replace with your deployment)
- **OpenAPI schema:** [openapi.json](./openapi.json) · interactive docs at `/docs`
- **Pricing:** [PRICING.md](./PRICING.md)

---

## Authentication

Pass your API key on every screening request:

```http
X-API-Key: your_api_key
```

Optional tracing header (recommended for support):

```http
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
```

The same `request_id` is returned in the `X-Request-ID` response header and in the JSON body.

| Key state | HTTP | Response |
|-----------|------|----------|
| Valid key | — | Normal response |
| Missing key | — | Anonymous tier (30 req/min) |
| Unknown key | 401 | `invalid_api_key` |
| Disabled key | 403 | `api_key_disabled` |

---

## Screen individual

`POST /api/v1/screen`

```bash
curl -sS -X POST "https://api.example.com/api/v1/screen" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -H "X-Request-ID: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{
    "first_name": "Vladimir",
    "last_name": "Putin",
    "dob": "1952-10-07",
    "country": "Russia",
    "min_score": 85,
    "lists": ["SDN", "CONSOLIDATED"]
  }'
```

Use any combination of name parts (`first_name`, `last_name`, `middle_name`) or `full_name`. At least one search field is required.

---

## Screen entity

`POST /api/v1/screen`

```bash
curl -sS -X POST "https://api.example.com/api/v1/screen" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -d '{
    "full_name": "Gazprombank",
    "country": "Russia",
    "swift_bic": "GAZPRUMM",
    "min_score": 85,
    "lists": ["SDN", "CONSOLIDATED"]
  }'
```

Entity-friendly fields include `full_name`, `registration_number`, `tax_id`, `swift_bic`, `imo_number`, `website`, and address fields. See [openapi.json](./openapi.json) for the full request schema.

---

## Batch screening

`POST /api/v1/screen/batch`

Each item in the batch counts as **one billing unit** (one check), not one HTTP request.

```bash
curl -sS -X POST "https://api.example.com/api/v1/screen/batch" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -d '{
    "items": [
      {
        "first_name": "John",
        "last_name": "Smith",
        "dob": "1980-01-15",
        "min_score": 85
      },
      {
        "full_name": "Acme Trading LLC",
        "country": "United Arab Emirates",
        "min_score": 85
      }
    ]
  }'
```

Returns a JSON **array** of screening responses (one per item). Maximum batch size is enforced by the server (default 100).

---

## Rate limits

Limits are tracked per **API key**, not per IP. Batch requests consume `len(items)` units.

### Response headers

**Per minute**

| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Minute quota for your plan |
| `X-RateLimit-Remaining` | Checks remaining this minute |
| `X-RateLimit-Reset` | Unix timestamp when the minute window resets |

**Per month**

| Header | Description |
|--------|-------------|
| `X-RateLimit-Month-Limit` | Monthly check quota |
| `X-RateLimit-Month-Remaining` | Checks remaining this month |
| `X-RateLimit-Month-Reset` | Unix timestamp when the month window resets |

### 429 Too Many Requests

When either limit is exceeded:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1780640400
X-RateLimit-Month-Limit: 5000
X-RateLimit-Month-Remaining: 4823
X-RateLimit-Month-Reset: 1782950400
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
```

```json
{
  "error": "rate_limit_exceeded",
  "message": "Too many requests",
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "retry_after_seconds": 42
}
```

Wait at least `retry_after_seconds` (or use `Retry-After`) before retrying. See [PRICING.md](./PRICING.md) for plan quotas.

---

## Response fields

Top-level screening response:

| Field | Type | Description |
|-------|------|-------------|
| `request_id` | string | Unique trace ID (also in `X-Request-ID`) |
| `status` | string | `clear`, `potential_match`, or `match` |
| `search_strategy` | string | Strategy used (`combined` by default) |
| `search_mode` | string | `exact` or `discovery` |
| `query` | object | Normalized query echo |
| `matches` | array | Ranked matches (empty if `clear`) |
| `screened_at` | datetime | UTC timestamp |
| `data_version` | string | OFAC publication version |
| `records_loaded` | integer | Records searched |

Each item in `matches`:

| Field | Type | Description |
|-------|------|-------------|
| `score` | number | Weighted combined score (0–100) |
| `matched_name` | string | Best matching OFAC name |
| `entity_id` | string | OFAC entity ID |
| `list_type` | string | `SDN` or `CONSOLIDATED` |
| `party_type` | string | e.g. `Individual`, `Entity` |
| `programs` | array | Sanctions programs |
| `matched_fields` | object | Per-field scores (`name`, `dob`, `country`, …); `null` = ignored |
| `ignored_fields` | array | Fields skipped (no candidate data) |
| `mismatched_fields` | array | Fields that reduced the score |
| `explain` | object | Human-readable breakdown per field |

### Example (abbreviated)

```json
{
  "request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "status": "match",
  "search_strategy": "combined",
  "matches": [
    {
      "score": 96.86,
      "matched_name": "Putin Vladimir Vladimirovich",
      "entity_id": "35096",
      "list_type": "SDN",
      "party_type": "Individual",
      "programs": ["RUSSIA-EO14024"],
      "matched_fields": { "name": 97.63, "dob": 95.0, "country": null },
      "ignored_fields": ["country"],
      "mismatched_fields": [],
      "explain": {
        "name": {
          "query": "Vladimir Putin",
          "candidate": "Putin Vladimir",
          "method": "token_set + wratio + phonetic",
          "score": 97.63
        },
        "dob": { "query": "1952-10-07", "candidate": "1952-10-7", "score": 95.0 },
        "country": {
          "query": "Russia",
          "candidate": null,
          "status": "ignored",
          "reason": "candidate has no country data"
        }
      }
    }
  ],
  "screened_at": "2026-07-04T08:00:00Z",
  "records_loaded": 19571
}
```

`min_score` (default 85) controls the `status` threshold: at or above → `match`, below → `clear` or `potential_match`.

---

## Errors

| HTTP | `error` | When |
|------|---------|------|
| 400 | — | Invalid query (`detail` field) |
| 401 | `invalid_api_key` | Unknown `X-API-Key` |
| 403 | `api_key_disabled` | Key revoked or rotated |
| 429 | `rate_limit_exceeded` | Minute or month quota exceeded |

**401 example**

```json
{
  "error": "invalid_api_key",
  "message": "Invalid API key",
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

**403 example**

```json
{
  "error": "api_key_disabled",
  "message": "API key is disabled",
  "request_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

**429 example** — see [Rate limits](#rate-limits) above.

---

## Other endpoints

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/health` | Service and list status |
| `GET` | `/api/v1/version` | Engine, matcher, and dataset version |
| `GET` | `/api/v1/meta/search-fields` | Available search fields |
| `GET` | `/api/v1/entities/{entity_id}` | Full OFAC entity profile |

Full schema: [openapi.json](./openapi.json)

Regenerate after API changes:

```bash
python scripts/export_openapi.py
```
