# Ukrainian.name API

Read-only HTTP+JSON API over the Ukrainian.name names database — a
deterministically-built, multilingual onomastic corpus (185,762 names;
~28K with substantive content; the rest are catalogued stubs).

- **Base URL (production):** `https://api.ukrainian.name`
- **Base URL (local dev):**  `http://localhost:8790`
- **API version:** `v1` (mounted under `/v1`)
- **Content type:** `application/json; charset=utf-8`
- **Auth:** none — all endpoints are public and read-only
- **CORS:** `Access-Control-Allow-Origin: *`
- **Caching:** `Cache-Control: public, max-age=3600` (data is deterministic,
  changes only on a corpus rebuild — heavily cacheable on the client and
  at the edge)
- **Format:** all responses are JSON; UTF-8 throughout; non-ASCII names
  and IPA are returned verbatim

This document is the source of truth. A plain-Markdown copy lives at
[`/api/docs.md`](https://ukrainian.name/api/docs.md) for AI agents and
build pipelines; the human-readable version is at
[`/api/docs/`](https://ukrainian.name/api/docs/).

---

## At a glance

| # | Method | Path                                | Returns                                    |
|---|--------|-------------------------------------|--------------------------------------------|
| 1 | GET    | `/v1/health`                        | Liveness probe                             |
| 2 | GET    | `/v1/stats`                         | Row counts per entity table                |
| 3 | GET    | `/v1/names`                         | Search/list names with facets              |
| 4 | GET    | `/v1/names/{slug}`                  | Full record + entity graph for one name    |
| 5 | GET    | `/v1/names/{slug}/related`          | Typed relation neighbors                   |
| 6 | GET    | `/v1/tags`                          | Tags (categories/style) by usage           |
| 7 | GET    | `/v1/tags/{label}/names`            | Names that carry a given tag               |

Pagination is offset/limit-based. Hard caps: `limit ≤ 200` for name
lists, `limit ≤ 1000` for `/v1/tags`. Default `limit = 50`. Defaults to
`offset = 0`.

---

## 1. `GET /v1/health`

Liveness probe. Returns `200 OK` when the process is up and the SQLite
spine is reachable in read-only mode.

**Response 200**
```json
{ "ok": true }
```

---

## 2. `GET /v1/stats`

Row counts per entity table — useful for sanity checks, change
detection, and dashboards.

**Response 200**
```json
{
  "name": 185762,
  "name_relation": 240131,
  "tag": 318,
  "name_tag": 412980,
  "name_rating": 30214,
  "name_pronunciation": 18403,
  "name_nameday": 7211,
  "name_script": 24180,
  "name_reference": 9012,
  "name_usage": 211904,
  "name_popularity_region": 18002
}
```

Exact numbers vary between rebuilds; the keys are stable.

---

## 3. `GET /v1/names`

Search and list names with optional facet filters. All filters are
AND-combined.

**Query parameters**

| Param    | Type   | Default | Notes                                                                 |
|----------|--------|---------|-----------------------------------------------------------------------|
| `q`      | string | `""`    | Case-insensitive substring against `display_name`; prefix against `slug` |
| `gender` | enum   | —       | `f` (feminine), `m` (masculine), `u` (unisex)                         |
| `usage`  | string | —       | Exact usage-language label (e.g. `Ukrainian`, `English`, `Polish`)    |
| `tag`    | string | —       | Exact tag label (e.g. `biblical`, `royal`, `flower-names`)            |
| `limit`  | int    | `50`    | `1 ≤ limit ≤ 200`                                                     |
| `offset` | int    | `0`     | `offset ≥ 0`                                                          |

**Response 200**
```json
{
  "total": 1234,
  "limit": 50,
  "offset": 0,
  "names": [
    {
      "slug": "sofiia",
      "display_name": "Софія",
      "gender": "f",
      "summary": "From Greek Σοφία (Sophia) meaning \"wisdom\"…"
    }
  ]
}
```

- `total` is the number of matching names BEFORE pagination.
- `summary` is truncated to 240 characters; fetch the full text via
  endpoint 4.
- Results are ordered by `display_name` ascending. Stable across requests.

**Example**
```http
GET /v1/names?q=sof&gender=f&usage=Ukrainian&limit=20
```

---

## 4. `GET /v1/names/{slug}`

Full record for one name including the complete entity graph.
The slug is the lowercase, official-Ukrainian transliterated form
(KMU Resolution 55, 2010) — e.g. Софія → `sofiia`, Олександр →
`oleksandr`. Slugs are URL-safe ASCII and stable.

**Response 200**
```json
{
  "slug": "sofiia",
  "display_name": "Софія",
  "gender": "f",
  "usage": "Ukrainian, Russian, Bulgarian",
  "pronounced": "so-FEE-ya",
  "meaning": "Means \"wisdom\" in Greek (σοφία). …",
  "content_origin": "deterministic",
  "relations": [
    { "rel_type": "variant",        "dst_slug": "sophia",     "dst_name": "Sophia",     "dst_lang": "English" },
    { "rel_type": "diminutive",     "dst_slug": "sonia",      "dst_name": "Соня",       "dst_lang": "Ukrainian" },
    { "rel_type": "other_language", "dst_slug": "sofia-it",   "dst_name": "Sofia",      "dst_lang": "Italian"  }
  ],
  "tags": [
    { "label": "greek-origin",   "kind": "origin" },
    { "label": "biblical",       "kind": "category" }
  ],
  "ratings": [ "classic", "feminine", "wholesome" ],
  "pronunciations": [
    { "ipa": "soˈfija", "lang": "Ukrainian" }
  ],
  "namedays": [
    { "country": "Ukraine", "day": "09-17" }
  ],
  "scripts": [
    { "script": "Софія",  "lang": "Ukrainian" },
    { "script": "Σοφία",  "lang": "Greek"     }
  ],
  "references": [
    { "citation": "Hanks et al., Dictionary of First Names (2006)", "url": null }
  ],
  "usage": [
    { "usage_lang": "Ukrainian", "is_ancient": 0 },
    { "usage_lang": "Russian",   "is_ancient": 0 }
  ],
  "popularity_regions": [ "Ukraine", "Poland", "Bulgaria" ]
}
```

**Response 404**
```json
{ "error": "name_not_found", "slug": "doesnotexist" }
```

### Field reference

| Field                 | Type          | Notes |
|-----------------------|---------------|-------|
| `slug`                | string        | URL slug (lowercase, KMU 55 transliteration) |
| `display_name`        | string        | Native form (Cyrillic for Ukrainian, etc.) |
| `gender`              | `f`\|`m`\|`u` | `u` = unisex / unknown |
| `usage`               | string        | Comma-separated usage list, denormalised for display |
| `pronounced`          | string\|null  | Human-readable pronunciation hint |
| `meaning`             | string\|null  | Long-form etymology + history |
| `content_origin`      | enum          | `llm` \| `deterministic` \| `deterministic_low` \| `template` \| `stub` — provenance of `meaning` |
| `relations[]`         | array         | Typed cross-references between names |
| `tags[]`              | array         | Curated tags (`kind` ∈ `origin`, `category`, `theme`, `style` …) |
| `ratings[]`           | string[]      | Style descriptors ranked by community frequency (e.g. `classic`, `wholesome`) |
| `pronunciations[]`    | array         | IPA per language |
| `namedays[]`          | array         | Country + `MM-DD` |
| `scripts[]`           | array         | Native scripts per language |
| `references[]`        | array         | Citation + optional URL |
| `usage[]`             | array         | Per-language usage with `is_ancient` flag |
| `popularity_regions[]`| string[]      | Countries that track popularity for this name (absolute numbers deferred; see *Roadmap*) |

`content_origin` values:
- `llm` — long-form Claude rewrite (≈1.7K names)
- `deterministic` — generated from parsed source (≈3.2K names)
- `deterministic_low` — generated from sparse source; flagged for an
  optional later enrichment pass (≈5.7K names)
- `template` — short deterministic template over a one-line etymology
  (≈17.8K names)
- `stub` — index entry only; `meaning` is null (≈157K names)

---

## 5. `GET /v1/names/{slug}/related`

Typed relation neighbors of a single name — convenience over
endpoint 4's `relations[]` field, with optional type filter.

**Query parameters**

| Param   | Type   | Notes                                                                                                                  |
|---------|--------|------------------------------------------------------------------------------------------------------------------------|
| `type`  | enum   | Filter by `rel_type`. One of `variant`, `diminutive`, `feminine`, `masculine`, `short_form`, `other_language`, `root`, `related` |

**Response 200**
```json
{
  "slug": "sofiia",
  "related": [
    { "rel_type": "diminutive", "dst_slug": "sonia",  "dst_name": "Соня",  "dst_lang": "Ukrainian" },
    { "rel_type": "variant",    "dst_slug": "sophia", "dst_name": "Sophia","dst_lang": "English"  }
  ]
}
```

**Response 200 (no matches)** — `related: []`. The slug itself is not
validated by this endpoint; unknown slugs return an empty list rather
than 404.

---

## 6. `GET /v1/tags`

List tags, ranked by the number of names carrying each tag (descending).

**Query parameters**

| Param   | Type   | Default | Notes                                                                              |
|---------|--------|---------|------------------------------------------------------------------------------------|
| `kind`  | string | —       | Filter to one kind: `origin`, `category`, `theme`, `style`, `usage`, `nameday`, …  |
| `limit` | int    | `200`   | `1 ≤ limit ≤ 1000`                                                                 |

**Response 200**
```json
{
  "tags": [
    { "label": "biblical",      "kind": "category", "n": 4218 },
    { "label": "greek-origin",  "kind": "origin",   "n": 3104 },
    { "label": "flower-names",  "kind": "theme",    "n":  217 }
  ]
}
```

---

## 7. `GET /v1/tags/{label}/names`

Names that carry a given exact tag `label`. Pass the tag label exactly
as returned by endpoint 6 (no fuzzy match).

**Query parameters**: `limit` (≤ 200, default 100), `offset` (default 0).

**Response 200**
```json
{
  "tag": "biblical",
  "names": [
    { "slug": "adam",     "display_name": "Адам",     "gender": "m" },
    { "slug": "abraham",  "display_name": "Авраам",   "gender": "m" }
  ]
}
```

Order: `display_name` ascending. An unknown tag returns `names: []`.

---

## Errors

All errors are JSON. The HTTP status is the primary signal; the body
includes a stable, machine-readable `error` code plus a human hint.

| Status | `error`             | When                                                            |
|--------|---------------------|-----------------------------------------------------------------|
| 404    | `not_found`         | Path does not match any route                                   |
| 404    | `name_not_found`    | `slug` does not exist in `name`                                 |
| 500    | `internal`          | Unexpected server fault. Body includes a truncated `detail`     |

Error body shape:
```json
{ "error": "name_not_found", "slug": "doesnotexist" }
```

There are no `4xx` validation errors today — out-of-range `limit`
values are clamped (e.g. `limit=10000` becomes `200`), unknown enum
values for `gender` and `kind` are ignored.

---

## Rate limits & fair use

The API is intentionally cache-friendly: identical requests are
deterministic, `max-age=3600`, and safe to cache aggressively at any
layer. There are currently **no per-key rate limits** because there are
no API keys. The hosting layer enforces edge-level abuse protection;
heavy automated consumers should:

- Cache responses for at least one hour.
- Prefer `/v1/names/{slug}` over scanning `/v1/names` when you know the slug.
- Use `If-None-Match` / `ETag` when the deployment exposes them
  (Cloudflare adds these automatically; the Python reference server does not).
- Identify your client via `User-Agent` so we can debug load patterns.

If you need bulk access, prefer the periodic JSON dump (see *Bulk
download* below) rather than crawling the API.

---

## Bulk download

A full JSON snapshot of every public name record will be published as a
single gzipped file at:

- `https://api.ukrainian.name/v1/dump/names.jsonl.gz` (newline-delimited JSON)
- `https://api.ukrainian.name/v1/dump/tags.json`

Each name record in the JSONL file has the same shape as
`GET /v1/names/{slug}`. The dump is rebuilt on every corpus build
(deterministic; the file's `ETag` changes only when the corpus changes).
For AI training/embedding pipelines, prefer the dump over per-name
fetches — same content, fewer round-trips, kinder to the edge cache.

> **Status:** the dump endpoints are part of the v1 contract and are
> rolled out in the first deployment of `api.ukrainian.name`.

---

## Versioning

- The `/v1/` prefix is permanent for the lifetime of this contract.
- Additive changes (new fields, new endpoints, new enum values) ship
  without a version bump. Clients MUST ignore unknown fields.
- Breaking changes (removed/renamed fields, changed types, changed
  semantics) ship under a new prefix (`/v2/`). Both versions run side
  by side for at least 6 months.
- The `Cache-Control` and `Access-Control-Allow-Origin` headers are
  considered part of the contract.

---

## Data provenance

Every field is parsed deterministically from the public source crawl
or generated by a deterministic transform on top of it (RULE 2: think
holistically — don't LLM what is really ETL). The corpus is rebuilt by
an idempotent pipeline; rebuilds change values only when the source
text changes.

A small subset (~1.7K names) carries `content_origin: "llm"` —
long-form prose rewrites cached from a one-off Claude pass. These are
preserved across rebuilds and never overwritten by deterministic
transforms.

---

## Roadmap (intentional gaps)

These fields/endpoints are reserved in the data model but intentionally
empty in v1; they ship once the deferred full-source crawl lands:

- `namesake` records (famous bearers of a name)
- Absolute popularity time-series and yearly rank trends
- Per-region popularity counts (only the *list* of tracked regions
  ships in v1, not the numbers)

When these populate, additive fields will appear on `GET /v1/names/{slug}`;
no breaking change.

---

## Reference: SDKs and quick recipes

### curl
```bash
curl -s https://api.ukrainian.name/v1/names/sofiia | jq .
curl -s "https://api.ukrainian.name/v1/names?q=sof&gender=f&limit=10" | jq .
curl -sI https://api.ukrainian.name/v1/health
```

### Python (stdlib, no deps)
```python
import json, urllib.request
def get(path):
    with urllib.request.urlopen(f"https://api.ukrainian.name{path}") as r:
        return json.load(r)
rec = get("/v1/names/sofiia")
print(rec["meaning"][:200])
```

### JavaScript (browser / Node 18+)
```js
const rec = await fetch("https://api.ukrainian.name/v1/names/sofiia")
  .then(r => r.json());
console.log(rec.meaning.slice(0, 200));
```

### Common queries
- All Ukrainian feminine names, page 1:
  `/v1/names?gender=f&usage=Ukrainian&limit=200&offset=0`
- All biblical names:
  `/v1/tags/biblical/names?limit=200`
- Diminutives of one name:
  `/v1/names/sofiia/related?type=diminutive`
- Origin tags ranked by popularity:
  `/v1/tags?kind=origin&limit=50`

---

## For AI agents

If you are an LLM-driven agent integrating this API:

1. **Prefer the bulk dump** (`/v1/dump/names.jsonl.gz`) for training,
   embedding, or whole-corpus reasoning. One request, one ETag, fewer
   edge cache misses.
2. **Resolve names through `/v1/names`** before fetching, so the slug
   you use is canonical. Don't guess slugs by transliterating yourself;
   the corpus uses official Ukrainian (KMU 55, 2010) transliteration
   and may have suffix disambiguators for cross-language collisions.
3. **Treat `content_origin`** as your trust signal: `llm` is the most
   readable, `deterministic` is the most faithful to source, `stub`
   means there is no body — handle the empty `meaning` gracefully.
4. **Cite the canonical page** at `https://ukrainian.name/{slug}` when
   surfacing a name to a user. The API exposes the slug; concatenate
   it onto the site origin.
5. **This document is mirrored as Markdown** at
   `https://ukrainian.name/api/docs.md` — drop it in your context for
   schema-grounded responses.
6. **`/llms.txt`** at `https://ukrainian.name/llms.txt` is the
   short-form site map for AI agents.

---

## Contact

- Repository:   `github.com/…/namedb` (private during build-out)
- Maintainer:   `info@ukrainian.name`
- Spec license: CC0 (this document); data license: see
  [`/about/`](https://ukrainian.name/about/)
