{"openapi":"3.1.0","info":{"title":"EthniAfrica API v2 - AFRIK","version":"2.3.0","description":"API publique v2 basée sur la méthodologie AFRIK. Identifiants stables (FLG_*, PPL_*, codes ISO 3166-1 alpha-3) et format de réponse standardisé avec pagination. Cette API fournit un accès structuré aux données ethnographiques et linguistiques de l'Afrique.\n\n## Authentication and rate limits\n\nNo key is required. A request without an `Authorization` header is served on the **anonymous** tier, metered at 60 requests per minute per client IP.\n\nAn API key passed as `Authorization: Bearer <key>` selects a larger quota: `public` keys 600 requests per minute, `partner` keys 6,000, `admin` keys unmetered. A key that is present but unknown, revoked or expired is refused with `401 invalid_api_key` rather than served anonymously, so a broken integration fails loudly instead of silently dropping to the anonymous quota.\n\nMetered responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; an exhausted quota answers `429` with `Retry-After`. The figures above are the deployment defaults. The `/api/v2/keys` self-service endpoints are the exception to all of this: they authenticate a signed-in contributor's session instead.\n\n## Response envelope\n\nEvery `/api/v2/*` response uses the Module #0 envelope: `{ data, meta: { license, attribution, pagination?, confidence?, pinned_url?, translation? }, errors }`. License and attribution are always present (AR8); `errors` is empty on success and populated on non-2xx responses. List endpoints place their pagination values under `meta.pagination`.\n\n## French only (breaking)\n\nThe site publishes French alone, so the locale parameters that selected English are narrowed to `fr`: `lang` on `GET /api/v2/search` and `GET /api/v2/search/companions`, and `language` in the `POST /api/v2/flags` body. `lang=en` is now refused with 400 on the two searches; an unknown flag `language` still fails closed to French. Search no longer matches English names; the `nameEn` / `languageFamilyNameEn` fields keep their shape and still carry the corpus value. `contentLanguage` in companion payloads is always `fr`.\n\n## 2.3.0 — translated records (additive)\n\nThe five single-entity endpoints accept `?lang=fr|en`. `fr` is the authored language and the default; `en` overlays the translation record when one exists and declares how it was produced in `meta.translation` — `human`, `machine_reviewed` or `machine` — with `stale: true` when the French moved on a field the reader is shown since it was translated. Fields whose subject is a word are withheld at `machine` provenance and served in French until a human has reviewed them (REQ-142, REQ-143). Lists, facets and search stay authored-French. Retired since: the corpus is no longer translated, `?lang` is ignored and every record is served as authored.\n\n## 2.1.0 — one source-tier vocabulary (breaking)\n\nSource authority is now one three-value scale — `official` | `referenced` | `unverified` — spoken identically by the database, the payloads and the UI. Provenance stays on the separate `source_kind` axis, so AI-generated text is `tier: \"unverified\"` + `source_kind: \"ai_generated\"` rather than a tier of its own.\n\nRemoved, all superseded by `tier`:\n\n- the numeric evidence-tier property on `Source`, `ReferenceSource` and `ReferenceCreateInput` (a `1 | 2 | null` scale that could not express an unverified source);\n- the legacy `Source.type` enum, which read a column dropped in migration 015 and was therefore always `null`;\n- the two catalogue gating properties on `Source.policy`, which decided whether a citation could be published at all. Under the source doctrine no citation is refused, so `Source.policy.tier` carries the signal instead.","contact":{"name":"EthniAfrica","url":"https://github.com/big-emotion/ethniafrica"}},"servers":[{"url":"https://ethniafrica.com","description":"Serveur de développement"}],"tags":[{"name":"API v2 - Search","description":"Recherche multi-entités (API v2)"},{"name":"API v2 - Countries","description":"Opérations sur les pays (API v2)"},{"name":"API v2 - Peoples","description":"Opérations sur les peuples (API v2)"},{"name":"API v2 - Language Families","description":"Opérations sur les familles linguistiques (API v2)"},{"name":"API v2 - Languages","description":"Opérations sur les langues (API v2)"},{"name":"API v2 - Keys","description":"API key management (issuance)"},{"name":"API v2 - Source Transparency","description":"Source Transparency Fabric — sources, confidence scores, editorial doctrine"},{"name":"API v2 - Oral Narratives","description":"Public, attributed oral narratives. Restricted narratives and protected metadata are never returned."},{"name":"API v2 - Media","description":"Media credits (author, licence URI, source page) attached to a fiche. Metadata only — never binary media content (REQ-128)."},{"name":"API v2 - Feed","description":"Revision feed — cursor-paginated Atom + JSON feed of recent published revisions (FR38, AR19, NFR32)"},{"name":"API v2 - Flags","description":"Editorial reports on AFRIK entities. Submission is open — a bearer token is optional and decides attribution only. The control is a proof of work computed in the reader's browser and verified here, so no visitor data reaches a third party (moderation charter §2)."},{"name":"API v2 - Reference Library","description":"Authenticated contributor workspace for structured references, assertion locators, and private working assets."},{"name":"API v2 - Names","description":"Name-variant records (endonyms, exonyms, historical spellings, surnames) — browsable, filterable, searchable index (FR53, FR55, FR58)."},{"name":"API v2 - Patronymes","description":"Family names (patronymes) — a name's naming system, caste or social function, associated peoples and countries, and bearers. Distinct from API v2 - Names (the ethnonym dossier); bearer entries are a narrow allow-listed summary and never carry ethnic-origin data for a named living person (DEC-040, REQ-133)."},{"name":"API v2 - Dossiers","description":"Long-form sourced dossiers — the one corpus entity that argues rather than describes. Every chapter carries an authoritative reading of a fact and a counter-reading of the same fact, each cited, and the schema refuses to serve a chapter holding only one of the two."},{"name":"API v2 - Compare","description":"Comparison of 2–3 entities of the same type (peoples, countries, or language families), reusing the same assembly path as the SSR comparison page (FR64, AR8, AR9, NFR38)."},{"name":"API v2 - Relations","description":"Sourced inter-people relations plus read-time-computed derived linguistic links (Epic 11, FR73)."},{"name":"API v2 - Migrations","description":"Spatio-temporal migration events (Bantu expansion phases, trade routes, forced displacements, pastoral movements) with GeoJSON geometry, time range, peoples, sources, and confidence (Epic 12, FR83, AR8/AR9)."},{"name":"API v2 - Quiz","description":"Smart quiz engine — audience segments with per-rung question counts, and randomly-composed sessions drawn from the verified AFRIK corpus, gate-checked at serve time (Epic 10, FR65/FR66, AR8/AR9, NFR38)."},{"name":"API v2 - Places","description":"Places (towns, regions, historic sites — LOC_*) and the history of their names: every name a place answers to, in the shared nameHistory block every named subject carries (REQ-196, ARCH-028)."},{"name":"API v2 - Words","description":"Words (WRD_*) whose history explains Africa through its names, each told in the shared nameHistory block (REQ-196, ARCH-028)."}],"paths":{"/api/v2/reference-library":{"get":{"summary":"Search structured references","description":"Searches the reference library. Open to any signed-in session; creating references, linking them to assertions and uploading working assets are reserved to moderators. The result is mutable and is never cached.","tags":["API v2 - Reference Library"],"security":[{"SupabaseJwtAuth":[]}],"parameters":[{"in":"query","name":"q","required":true,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"Reference search term.","example":"Ethnologue"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of matching references to return."}],"responses":{"200":{"description":"Matching references.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferenceSearchResponse"}}}},"400":{"description":"Invalid search query.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"401":{"description":"Missing or invalid Supabase access token.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}},"post":{"summary":"Create or resolve a structured reference","description":"Creates a structured reference or returns an existing matching entry. Reserved to moderators: a signed-in session whose address is not on the moderator allowlist is refused with 403. The result is mutable and is never cached.","tags":["API v2 - Reference Library"],"security":[{"SupabaseJwtAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferenceCreateInput"}}}},"responses":{"200":{"description":"Existing matching reference returned.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferenceCreateResponse"}}}},"201":{"description":"Reference created.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReferenceCreateResponse"}}}},"400":{"description":"Invalid request body.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"401":{"description":"Missing or invalid Supabase access token.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"403":{"description":"Signed-in session whose address is not on the moderator allowlist.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/reference-library/assertions":{"post":{"summary":"Link a reference to an assertion","description":"Creates a structured locator linking a library reference to an assertion. Reserved to moderators: a signed-in session whose address is not on the moderator allowlist is refused with 403. The result is mutable and is never cached.","tags":["API v2 - Reference Library"],"security":[{"SupabaseJwtAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssertionReferenceCreateInput"}}}},"responses":{"201":{"description":"Assertion-reference link created.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssertionReferenceCreateResponse"}}}},"400":{"description":"Invalid request body.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"401":{"description":"Missing or invalid Supabase access token.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"403":{"description":"Signed-in session whose address is not on the moderator allowlist.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/reference-library/assets":{"post":{"summary":"Upload a private reference working asset","description":"Stores a private scan or OCR working asset. Reserved to moderators: a signed-in session whose address is not on the moderator allowlist is refused with 403. The upload is never cached. The response contains metadata only: it omits binary content, storage bucket identifiers, and object or location paths, and it does not grant retrieval access.","tags":["API v2 - Reference Library"],"security":[{"SupabaseJwtAuth":[]}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/ReferenceWorkingAssetCreateInput"}}}},"responses":{"201":{"description":"Private asset metadata created; binary content and storage locations are intentionally omitted.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateReferenceWorkingAssetResponse"}}}},"400":{"description":"Invalid multipart form data.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"401":{"description":"Missing or invalid Supabase access token.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"403":{"description":"Signed-in session whose address is not on the moderator allowlist.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/words":{"get":{"summary":"List words (WRD_*)","description":"One page of the word fiches — words whose history explains Africa through its names — ordered by name (REQ-196). Each row is a summary; the word's names and nameHistory are served by `/api/v2/words/{id}`.\n","tags":["API v2 - Words"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Paginated word summaries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordListEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/words/{id}":{"get":{"summary":"Get a word by identifier","description":"One word fiche (REQ-196): every name it answers to, its shared nameHistory block (ARCH-028), its definition, the subjects the fiche ties to it, with the fiche's gaps and sources.\n","tags":["API v2 - Words"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^WRD_[A-Z0-9_]+$"},"example":"WRD_RACE"}],"responses":{"200":{"description":"Word detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WordDetailEnvelope"}}}},"400":{"description":"Invalid word identifier format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Word not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/sources":{"get":{"summary":"List Module","description":"Returns a paginated list of sources backing AFRIK assertions.","tags":["API v2 - Source Transparency"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"q","description":"Substring match over title and author.","schema":{"type":"string"}},{"in":"query","name":"tier","description":"Authority the source carries. `needs_review` is not a fourth tier: it selects the sources that carry none, which the corpus keeps distinct from `unverified`.","schema":{"type":"string","enum":["official","referenced","unverified","needs_review"]}},{"in":"query","name":"sourceKind","description":"Provenance of the source, recorded on few rows so far.","schema":{"type":"string"}},{"in":"query","name":"decade","description":"First year of a decade; matches years in [decade, decade + 10).","schema":{"type":"integer"}},{"in":"query","name":"sort","schema":{"type":"string","enum":["title","year","added"],"default":"title"}}],"responses":{"200":{"description":"Paginated list of sources","headers":{"Cache-Control":{"schema":{"type":"string","example":"public, s-maxage=86400, stale-while-revalidate=86400"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/sources/{id}":{"get":{"summary":"Get a single Module","tags":["API v2 - Source Transparency"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"200":{"description":"Source envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SourceResponse"}}}},"400":{"description":"Invalid id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Source not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/search":{"get":{"summary":"Search the main corpus or the dedicated quiz lens","description":"Full-text search, ranked in Postgres by `afrik_search_peoples` / `afrik_search_countries` (migration 052): an accent-insensitive exact name match first, then `ts_rank` over the weighted `search_vector` (migration 043: A = name and autonym, B = exonyms, C/D = prose) OR the accent-insensitive `name_unaccent_vector`, both queried with the `:*` prefix operator on the last word of `q` — so a partial trailing word (\"bamba\") matches a longer indexed name (\"Bambara\"), and an unaccented query (\"mande\") matches an accented name (\"Mandé\") by construction, not by stemmer coincidence (REQ-129). Relevance is multiplied for peoples by a 0.5–1.0 confidence factor. Language families rank in SQL too (migration 068): exact > prefix > substring on the folded name, then a tier for a match found only in their decolonial prose. `q` no longer accepts websearch syntax — quoted phrases, `OR` and `-` exclusions are not recognised (DEC-034); each word except the last must now match as a complete word. Each result carries `relevance`, `exactMatch` and a `snippet` whose matched terms are wrapped in `[[` and `]]` — deliberately not HTML, because `ts_headline` does not escape the source document. `relevance` is comparable within an array and not between arrays. Each entity kind is returned in its own array, and `results` carries the same hits merged into one cross-kind ranking on `normalizedScore` (migration 068) — the magnitude that, unlike `relevance`, is comparable between kinds. Without `lens`, quiz questions are not queried or returned in the main search stream; with `lens=quiz`, only quiz questions are queried and returned (REQ-121). The dedicated quiz projection contains the stem only: options, the correct answer and the explanation are never returned. Named persons (migration 065, REQ-126) and names (patronymes — migration 066, REQ-135) rank the same way, with patronymes additionally folding in a dmetaphone phonetic match so a misspelling like \"Keyta\" still reaches the canonical \"Keïta\". Both fall back to pg_trgm typo tolerance ahead of a no-match. Languages (migration 068, REQ-136) rank the same prefix/accent-insensitive way, with an exact-match bonus that also fires on the ISO 639-3 id — a reader who types \"swa\" reaches Swahili exactly as precisely as one who types its name. When `total` is 0, `leads` (REQ-125) carries up to 3 near-miss suggestions across peoples, countries and language families — a trigram similarity scan (migration 069) below the main search's own fuzzy floor, so the reader sees what the engine almost understood instead of a bare empty result; `leads` is always empty when `total` is greater than 0. The search is served in French. Rate-limited per AR11 (IP: 60 RPM, public key: 600 RPM, partner key: 6 000 RPM).\n","tags":["API v2 - Search"],"security":[{"BearerAuth":[]},{}],"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string","minLength":1},"description":"Full-text search query. Matches by prefix on its last word and is accent-insensitive throughout (REQ-129) — no websearch syntax: quoted phrases, `OR` and `-` exclusions are not recognised (DEC-034). Required unless familyId or countryId is given — a relation scope is a complete search on its own.\n","example":"Yoruba Nigeria"},{"in":"query","name":"lens","required":false,"schema":{"type":"string","enum":["quiz"]},"description":"Selects the dedicated, exclusive quiz-question stream. With `lens=quiz`, only quiz questions are queried and returned. Without this parameter, quiz questions are not queried and remain excluded from the main search stream.\n"},{"in":"query","name":"lang","required":false,"schema":{"type":"string","enum":["fr"]},"description":"Locale the search is served in. French is the only one published: absent or `fr` serves French, and any other value — `en` included, since the English site was retired — is rejected with 400 rather than defaulted.\n","example":"fr"},{"in":"query","name":"familyId","schema":{"type":"string","pattern":"^FLG_[A-Z0-9_]+$"},"description":"Scope peoples to one language family. With q it narrows the ranking; alone it lists that family's peoples, and countries and families then come back empty because nothing was asked of them.\n","example":"FLG_KROU"},{"in":"query","name":"countryId","schema":{"type":"string","pattern":"^[A-Z]{3}$"},"description":"Scope peoples to one country (ISO 3166-1 alpha-3)","example":"CIV"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"description":"Maximum results to return (capped at 50)"},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0},"description":"Number of results to skip"},{"in":"query","name":"classificationStatus","schema":{"type":"string","enum":["consensual","contested","colonial-legacy","reconstructive"]},"description":"Filter peoples by epistemic classification status"},{"in":"query","name":"minConfidence","schema":{"type":"number","minimum":0,"maximum":1},"description":"Filter by minimum confidence score (0–1)"},{"in":"query","name":"sinceVerifiedAfter","schema":{"type":"string","format":"date"},"description":"Filter to entities verified (human audit) after this ISO date","example":"2026-01-01"}],"responses":{"200":{"description":"Search results with Module","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"Invalid query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEnvelope"}}}},"429":{"description":"Rate limit exceeded (AR11)","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiEnvelope"}}}}}}},"/api/v2/search/companions":{"get":{"summary":"Resolve sourced companion content for search subjects","description":"Returns shorts, anecdotes, attested proverbs and one playable quiz question for up to twenty typed subjects and their direct corpus neighbours. Ordering is stable: requested subjects first, then linked family, people and country targets; duplicate items are removed after their strongest match is chosen. With no subjects, only the recent-short fallback may be populated. Well-formed unknown ids are treated as an empty subject rather than guessed or rejected. Every item carries its own relation code; localised relation labels are not part of the API. Successful responses are cached for one hour.\n","tags":["API v2 - Search"],"security":[{"BearerAuth":[]},{}],"parameters":[{"in":"query","name":"subjects","required":false,"schema":{"type":"string"},"description":"Comma-separated type:id values. Types are people, country, languageFamily, language and patronyme. At most 20 unique typed subjects are accepted; duplicates are removed in first-seen order.\n","example":"people:PPL_BASSA,country:CMR"},{"in":"query","name":"word","required":false,"schema":{"type":"string","maxLength":80},"description":"The word the reader typed, as typed. It finds the shorts filed against that word, for a word that is not a corpus entity (such as \"zombie\"); those items carry the relation \"word\" and lead the shelf. It is compared whole, lowercased and without accents, never by prefix.\n","example":"zombie"},{"in":"query","name":"lang","required":false,"schema":{"type":"string","enum":["fr"],"default":"fr"},"description":"Content locale. French is the only one published; any other value, `en` included, is rejected with 400.\n","example":"fr"}],"responses":{"200":{"description":"Bounded companion selections with item-level provenance","headers":{"Cache-Control":{"description":"Shared one-hour corpus cache","schema":{"type":"string","example":"s-maxage=3600"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchCompanionsResponse"},"examples":{"exact":{"summary":"One exact short","value":{"data":{"subjects":[{"entityType":"country","entityId":"NGA"}],"shorts":{"count":1,"items":[{"id":"short-nigeria","href":"/fr/decouvertes/nigeria","name":"Nigeria","description":"Une production sourcée.","publishedAt":"2026-09-01","durationSeconds":47,"watchUrl":"https://www.youtube.com/watch?v=example","poster":{"src":"/posters/nigeria.jpg","alt":"Couverture : D’où vient le nom « Nigeria » ?","width":270,"height":480},"source":{"title":"Source","url":"https://example.org/source","tier":"referenced"},"match":{"relation":"exact","entityType":"country","entityId":"NGA"}}]},"anecdotes":{"count":0,"items":[]},"proverbs":{"count":0,"items":[]},"quiz":{"count":0,"item":null}},"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[]}},"widened":{"summary":"One ring-1 anecdote","value":{"data":{"subjects":[{"entityType":"people","entityId":"PPL_EKPEYE"}],"shorts":{"count":0,"items":[]},"anecdotes":{"count":1,"items":[{"id":"nigeria-name","contentLanguage":"fr","headline":"Le nom du Nigeria vient du fleuve Niger.","body":["Le nom a été proposé à la fin du XIXe siècle."],"tier":"referenced","sources":[{"title":"Source","url":"https://example.org/source","tier":"referenced"}],"illustration":{"src":"/images/nigeria.jpg","alt":"Carte du Nigeria.","credit":"Domaine public"},"match":{"relation":"linked-country","entityType":"country","entityId":"NGA"}}]},"proverbs":{"count":0,"items":[]},"quiz":{"count":0,"item":null}},"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[]}},"sparse":{"summary":"Known subject with no eligible companion","value":{"data":{"subjects":[{"entityType":"patronyme","entityId":"PAT_RARE"}],"shorts":{"count":0,"items":[]},"anecdotes":{"count":0,"items":[]},"proverbs":{"count":0,"items":[]},"quiz":{"count":0,"item":null}},"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[]}},"empty":{"summary":"No resolved subject and no recent production","value":{"data":{"subjects":[],"shorts":{"count":0,"items":[]},"anecdotes":{"count":0,"items":[]},"proverbs":{"count":0,"items":[]},"quiz":{"count":0,"item":null}},"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[]}}}}}},"400":{"description":"Invalid subject syntax, subject count or locale","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Companion relation or quiz service failure","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/relations":{"get":{"summary":"List sourced relations","description":"Non-ego-centered, paginated list of sourced relations, filterable by relationType, peopleId (either side) and period window.\n","tags":["API v2 - Relations"],"parameters":[{"in":"query","name":"types","schema":{"type":"string"},"description":"Comma-separated relationType filter (migratory,commercial,religious)"},{"in":"query","name":"peopleId","schema":{"type":"string","pattern":"^PPL_[A-Z0-9_]+$"},"description":"Filter to relations involving this people (either side)"},{"in":"query","name":"periodFrom","schema":{"type":"integer"}},{"in":"query","name":"periodTo","schema":{"type":"integer"}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated relation list envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelationListResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600 (people-data class, AR18)","schema":{"type":"string"}}}},"400":{"description":"Invalid query params","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/relations/{id}":{"get":{"summary":"Get a single sourced relation","tags":["API v2 - Relations"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^REL_[A-Z0-9_]+$"},"description":"Identifiant de la relation (format REL_*)","example":"REL_SONINKE_MANDE_TRADE"}],"responses":{"200":{"description":"Relation detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RelationDetailResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600 (people-data class, AR18)","schema":{"type":"string"}}}},"400":{"description":"Invalid relation id format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Relation not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/quiz/session":{"get":{"summary":"Compose a quiz session for one entity scope","description":"Draws `count` (5–10, default 8) active questions from the scope named by `pays`, `famille` or `mode`, ordered by the games charter's difficulty ladder — the subject's population decile inside that scope, two easy rounds, four middling, two hard. `mode=aleatoire` is the one track that skips the ladder. Every question is re-validated at serve time against current confidence, human-audit and source-tier state (FR65 gate), so a question that has decayed since the last generation sweep never reaches a player. The answer key ships in the payload (`correctOption`, `explanationFr`, `source`): reveal is client-side, there is nothing to cheat for (no leaderboard, no persistence, no stakes). A track whose pool cannot fill the session asked for answers 422: a short session is a track with no top rung, which the ladder above is the whole point of. A pool that was big enough and was then thinned by the FR65 gate still answers 200, with however many questions survived — including none, the calm empty state.\n","tags":["API v2 - Quiz"],"security":[],"parameters":[{"in":"query","name":"lang","required":false,"schema":{"type":"string","enum":["fr"],"default":"fr"},"description":"Authored question-bank locale. French is the only one published; any other value is rejected with 400.\n"},{"in":"query","name":"pays","required":false,"schema":{"type":"string"},"description":"ISO 3166-1 alpha-3 country code — the peoples of that country","example":"GHA"},{"in":"query","name":"famille","required":false,"schema":{"type":"string"},"description":"`FLG_*` language family id — the peoples of that family","example":"FLG_NIGERO_CONGOLAISE"},{"in":"query","name":"mode","required":false,"schema":{"type":"string","enum":["mixte","aleatoire"]},"description":"Whole-corpus track when no entity is named. `mixte` (the default) applies the difficulty ladder; `aleatoire` does not.\n"},{"in":"query","name":"theme","required":false,"schema":{"type":"string","enum":["noms","langues","parente-linguistique","territoire","rites-et-culture","croyances","royaumes-et-histoire","organisation","migrations"]},"description":"A domain of content. Composes with `pays` / `famille` rather than replacing them, so `?pays=ZAF&theme=croyances` is « les croyances des peuples d'Afrique du Sud ». Omitted, a session spreads over at most two rounds per theme.\n"},{"in":"query","name":"count","required":false,"schema":{"type":"integer","minimum":5,"maximum":10,"default":8},"description":"Number of questions requested"}],"responses":{"200":{"description":"Composed session envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizSessionResponse"}}},"headers":{"Cache-Control":{"description":"no-store","schema":{"type":"string"}}}},"400":{"description":"Malformed pays, famille, mode or count","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"422":{"description":"Well-formed scope naming a country or family that does not exist, or a track whose pool cannot fill the session requested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (AR11, 60 req/min anonymous)","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/quiz/scopes":{"get":{"summary":"List the entity tracks a quiz session can be drawn from","description":"Every country and every language family, each with the number of currently active questions its peoples hold, plus the two whole-corpus tracks (`mixed`, ordered by the difficulty ladder, and `random`, which is not). `playable` is false when a track cannot fill a session of eight — it is still listed and still counted, so the UI can be honest about what the corpus holds rather than hiding it. Counts change only on generation sweeps (Story 10.5).\n","tags":["API v2 - Quiz"],"security":[],"parameters":[{"in":"query","name":"lang","required":false,"schema":{"type":"string","enum":["fr"],"default":"fr"},"description":"Authored question-bank locale. French is the only one published; any other value is rejected with 400.\n"}],"responses":{"200":{"description":"Scopes envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuizScopesResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600","schema":{"type":"string"}}}},"429":{"description":"Rate limit exceeded (AR11, 60 req/min anonymous)","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/places":{"get":{"summary":"List places (LOC_*)","description":"One page of the place fiches (towns, regions, historic sites), ordered by name (REQ-196). Each row is a summary; the place's names and nameHistory are served by `/api/v2/places/{id}`.\n","tags":["API v2 - Places"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Paginated place summaries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaceListEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/places/{id}":{"get":{"summary":"Get a place by identifier","description":"One place fiche (REQ-196): every name it answers to, its shared nameHistory block (ARCH-028), its country and the peoples the fiche associates with it, with the fiche's gaps and sources.\n","tags":["API v2 - Places"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^LOC_[A-Z0-9_]+$"},"example":"LOC_YAMOUSSOUKRO"}],"responses":{"200":{"description":"Place detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaceDetailEnvelope"}}}},"400":{"description":"Invalid place identifier format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Place not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples":{"get":{"summary":"Liste des peuples (paginée)","description":"Retourne la liste paginée de tous les peuples d'Afrique avec leurs données AFRIK","tags":["API v2 - Peoples"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1},"description":"Numéro de page","example":1},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Nombre d'éléments par page (max 100)","example":20},{"in":"query","name":"search","required":false,"schema":{"type":"string"},"description":"Full-text search applied to people records","example":"shona"},{"in":"query","name":"letter","required":false,"schema":{"type":"string","minLength":1,"maxLength":1},"description":"Case-insensitive initial letter filter","example":"S"},{"in":"query","name":"languageFamilyId","required":false,"schema":{"type":"string","pattern":"^FLG_[A-Z_]+$"},"description":"Language family identifier filter","example":"FLG_BANTU"}],"responses":{"200":{"description":"Liste paginée des peuples","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeoplesListEnvelope"}}}},"500":{"description":"Erreur serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples/{id}":{"get":{"summary":"Détails d'un peuple","description":"Retourne les détails complets d'un peuple par son identifiant PPL_*, et le bloc `patronymes` — les noms que porte ce peuple. À ne pas confondre avec `/peoples/{id}/names`, qui porte les ethnonymes : ce que le peuple est *appelé*, et non ce que ses membres *portent*.\n","tags":["API v2 - Peoples"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PPL_[A-Z_]+$"},"description":"Identifiant du peuple (format PPL_*)","example":"PPL_SHONA"}],"responses":{"200":{"description":"Détails du peuple","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeopleDetailEnvelope"}}}},"400":{"description":"Format d'identifiant invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Peuple non trouvé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Erreur serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples/{id}/versions/{n}":{"get":{"summary":"Pinned-version snapshot for a people entity","description":"Returns the full published snapshot at version n. Data is read exclusively from the immutable revision record — never from the live entity (AR14). The response carries Cache-Control: s-maxage=31536000, immutable because pinned data never changes (AR18).\n","tags":["API v2 - Peoples"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PPL_[A-Z_]+$"},"description":"People identifier (PPL_*)","example":"PPL_YORUBA"},{"in":"path","name":"n","required":true,"schema":{"type":"integer","minimum":1},"description":"Version number","example":3}],"responses":{"200":{"description":"Pinned-version snapshot envelope","headers":{"Cache-Control":{"description":"s-maxage=31536000, immutable — pinned data never changes","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeopleVersionSnapshotResponse"}}}},"400":{"description":"Invalid parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Version not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples/{id}/revisions":{"get":{"summary":"Revision history for a people entity","description":"Cursor-paginated list of published revisions ordered by version DESC. Implements AR10, AR14.","tags":["API v2 - Peoples"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PPL_[A-Z_]+$"},"description":"People identifier (PPL_*)","example":"PPL_YORUBA"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of revisions per page"},{"in":"query","name":"cursor","schema":{"type":"integer","minimum":1},"description":"Version number of the last item from the previous page (exclusive upper bound)"}],"responses":{"200":{"description":"Revision list envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeopleRevisionListResponse"}}}},"400":{"description":"Invalid parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples/{id}/relations":{"get":{"summary":"Ego-centered relation network for a people","description":"Returns the sourced relations directly attributed to this people plus, when includeDerived=true, the read-time-computed derived linguistic links (FR73). The sourced/derived split is structural, never a flag on a shared item shape.\n","tags":["API v2 - Relations"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PPL_[A-Z0-9_]+$"},"description":"Identifiant du peuple (format PPL_*)","example":"PPL_EWE"},{"in":"query","name":"types","schema":{"type":"string"},"description":"Comma-separated relationType filter (migratory,commercial,religious)"},{"in":"query","name":"includeDerived","schema":{"type":"boolean","default":true},"description":"Whether to populate the derived linguistic-link collection"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":24}}],"responses":{"200":{"description":"Ego-network envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EgoNetworkResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600 (people-data class, AR18)","schema":{"type":"string"}}}},"400":{"description":"Invalid people id or query params","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"People not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples/{id}/names":{"get":{"summary":"Names dossier for a people (endonyms, exonyms, historical spellings, surnames)","description":"Returns every name record for a people, ordered endonyms-first (sort_rank), then name_type, then name_text. Each record carries its own per-field sources[] and the people's confidence score, batched via the AR17 map pattern (no per-record queries).\n","tags":["API v2 - Peoples"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PPL_[A-Z0-9_]+$"},"description":"Identifiant du peuple (format PPL_*)","example":"PPL_DINKA"}],"responses":{"200":{"description":"Names dossier envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeopleNamesDossierResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600","schema":{"type":"string"}}}},"400":{"description":"Invalid people id format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"People not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/peoples/{id}/fragmentation":{"get":{"summary":"Colonial-border fragmentation for a people","description":"Derives, from data already in production (afrik_people_countries + content.demography.distributionByCountry), the countries a people spans with their demographic share and border pairs. `colonialOrigin` on a border pair is populated only once the colonial-borders dataset (Story 13.3) documents it (NFR31 — additive, optional from day one).\n","tags":["API v2 - Peoples"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PPL_[A-Z0-9_]+$"},"description":"Identifiant du peuple (format PPL_*)","example":"PPL_EWE"}],"responses":{"200":{"description":"Fragmentation envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PeopleFragmentationResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600 (people-data class, AR18)","schema":{"type":"string"}}}},"400":{"description":"Invalid people id format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"People not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"422":{"description":"People spans fewer than 2 countries — fragmentation undefined","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/patronymes/{id}":{"get":{"summary":"Get a name (patronyme) by identifier","description":"Returns the public details for one name (patronyme) — DEC-038's fifth corpus dimension: its naming system, its associated peoples and countries, and its bearers. Distinct from `/api/v2/names`, which serves the ethnonym dossier (the peoples' names).\n","tags":["API v2 - Patronymes"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^PAT_[A-Z0-9_]+$"},"description":"Patronyme identifier","example":"PAT_KEITA"}],"responses":{"200":{"description":"Patronyme detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatronymeDetailEnvelope"}}},"headers":{"Cache-Control":{"description":"Shared cache duration","schema":{"type":"string","example":"s-maxage=3600"}}}},"400":{"description":"Invalid patronyme identifier format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Patronyme not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/oral-narratives":{"get":{"summary":"List public oral narratives for a fiche","description":"Returns public, rights-cleared narratives whether reviewed or not, reviewed first, each with a reviewed flag. Rejected narratives are excluded. Responses omit transcripts, media locators, collector details, carrier references, reviewer identity, and withheld narrator names.","tags":["API v2 - Oral Narratives"],"parameters":[{"in":"query","name":"entityType","required":true,"schema":{"type":"string","enum":["language_family","people","country"]},"example":"people"},{"in":"query","name":"entityId","required":true,"schema":{"type":"string"},"example":"PPL_SHONA"},{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Paginated public oral narratives","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OralNarrativeListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/names":{"get":{"summary":"List and search name-variant records","description":"Browsable index of the peoples' names with optional full-text search via `websearch_to_tsquery('french', q)` on `search_vector`, ranked by relevance × confidence boost. Any name variant leads to its people (FR53, FR55, FR58).\n","tags":["API v2 - Names"],"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string","minLength":1},"description":"Full-text search query (websearch syntax, French stemming)","example":"jieng"},{"in":"query","name":"nameType","required":false,"schema":{"type":"string","enum":["endonym","exonym","historical_spelling","surname"]},"description":"Filter by name record type"},{"in":"query","name":"imposedOnly","required":false,"schema":{"type":"string","enum":["true","false"]},"description":"When true, only records with imposed_by IS NOT NULL return"},{"in":"query","name":"peopleId","required":false,"schema":{"type":"string","pattern":"^PPL_[A-Z_]+$"},"description":"Filter to name records belonging to one people","example":"PPL_JIENG"},{"in":"query","name":"letter","required":false,"schema":{"type":"string","minLength":1,"maxLength":1},"description":"Case-insensitive initial letter filter on nameText"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Names index with Module","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListNamesResponse"}}}},"400":{"description":"Invalid query parameter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (AR11)","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/migrations":{"get":{"summary":"List migration event summaries","description":"Paginated, filterable list of migration event summaries (no geometry — AR18 payload-size discipline). `from`/`to` bound a requested year window; an event is included when its own [startYear, endYear] intersects that window (interval intersection, not containment).\n","tags":["API v2 - Migrations"],"parameters":[{"in":"query","name":"from","schema":{"type":"integer"},"description":"Start of the requested year window (negative = BCE)"},{"in":"query","name":"to","schema":{"type":"integer"},"description":"End of the requested year window (negative = BCE)"},{"in":"query","name":"eventType","schema":{"type":"string","enum":["expansion","trade_route","forced_displacement","pastoral_movement","fragmentation","displacement","imposed_name","resistance"]}},{"in":"query","name":"peopleId","schema":{"type":"string","pattern":"^PPL_[A-Z0-9_]+$"},"description":"Filter to migrations involving this people"},{"in":"query","name":"classificationStatus","schema":{"type":"string","enum":["consensual","contested","colonial-legacy","reconstructive"]}},{"in":"query","name":"group","schema":{"type":"string"},"description":"Filter by migration_group (e.g. bantu-expansion)"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated migration summary list envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrationListResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600 (people-data class, AR18)","schema":{"type":"string"}}}},"422":{"description":"Invalid query params, or peopleId does not resolve to a known people","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/migrations/{id}":{"get":{"summary":"Get a single migration event","description":"Full migration event detail incl. GeoJSON geometry, time range, peoples involved (one batched join query, AR17), sources, narrative, debate, and meta.confidence.\n","tags":["API v2 - Migrations"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^MGR_[A-Z0-9_]+$"},"description":"Identifiant de l'événement migratoire (format MGR_*)","example":"MGR_BANTU_HOMELAND_DISPERSAL"}],"responses":{"200":{"description":"Migration detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrationDetailResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600 (people-data class, AR18)","schema":{"type":"string"}}}},"404":{"description":"Migration not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"422":{"description":"Invalid migration id format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/media":{"get":{"summary":"List media entries attached to a fiche","description":"Returns image/video credits (author, licence URI, source page) attached to a language family, language, people or country. Never returns binary media content — this endpoint is metadata only.","tags":["API v2 - Media"],"parameters":[{"in":"query","name":"entityType","required":true,"schema":{"type":"string","enum":["language_family","language","people","country"]},"example":"people"},{"in":"query","name":"entityId","required":true,"schema":{"type":"string"},"example":"PPL_SHONA"},{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1}},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Paginated media entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaListResponse"}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/languages/{id}":{"get":{"summary":"Get a language by ISO 639-3 identifier","description":"Returns the public details for one language.","tags":["API v2 - Languages"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^[a-z]{3}$"},"description":"Lowercase ISO 639-3 language identifier","example":"yor"}],"responses":{"200":{"description":"Language detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageDetailEnvelope"}}},"headers":{"Cache-Control":{"description":"Shared cache duration","schema":{"type":"string","example":"s-maxage=3600"}}}},"400":{"description":"Invalid language identifier format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Language not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/language-families":{"get":{"summary":"Liste des familles linguistiques (paginée)","description":"Retourne la liste paginée de toutes les familles linguistiques d'Afrique avec leurs données AFRIK","tags":["API v2 - Language Families"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1},"description":"Numéro de page","example":1},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Nombre d'éléments par page (max 100)","example":20}],"responses":{"200":{"description":"Liste paginée des familles linguistiques","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageFamiliesListEnvelope"}}}},"500":{"description":"Erreur serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/language-families/{id}":{"get":{"summary":"Détails d'une famille linguistique","description":"Retourne les détails complets d'une famille linguistique par son identifiant FLG_*","tags":["API v2 - Language Families"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^FLG_[A-Z_]+$"},"description":"Identifiant de la famille linguistique (format FLG_*)","example":"FLG_BANTU"}],"responses":{"200":{"description":"Détails de la famille linguistique","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageFamilyDetailEnvelope"}}}},"400":{"description":"Format d'identifiant invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Famille linguistique non trouvée","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Erreur serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/language-families/{id}/tree":{"get":{"summary":"Classification tree skeleton for a linguistic family","description":"Returns the family's languages (branches) with their linked people count, plus the count of peoples in the family not linked to any of its languages. Assembled from batched AFRIK queries (AR17, no N+1).\n","tags":["API v2 - Language Families"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^FLG_[A-Z_]+$"},"description":"Identifiant de la famille linguistique (format FLG_*)","example":"FLG_BANTU"}],"responses":{"200":{"description":"Family tree skeleton envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageFamilyTreeResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=86400","schema":{"type":"string"}}}},"400":{"description":"Invalid language family id format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Language family not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/language-families/{id}/tree/branch":{"get":{"summary":"Paginated branch of a language family's classification tree","description":"Lazily loads one branch of the family tree (people belonging to a given language, or the unlinked-peoples group) so large branches (e.g. Bantu) can be fetched page by page instead of inflating the initial tree payload (FR48, NFR3).\n","tags":["API v2 - Language Families"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^FLG_[A-Z_]+$"},"description":"Identifiant de la famille linguistique (format FLG_*)","example":"FLG_BANTU"},{"in":"query","name":"language","required":false,"schema":{"type":"string","pattern":"^[a-z]{3}$"},"description":"Code ISO 639-3 de la langue de la famille dont on veut les peuples. Mutuellement exclusif avec `group`.\n","example":"kon"},{"in":"query","name":"group","required":false,"schema":{"type":"string","enum":["unlinked"]},"description":"Sélectionne le groupe des peuples non rattachés à une langue de la famille. Mutuellement exclusif avec `language`.\n"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0,"default":0}}],"responses":{"200":{"description":"Paginated people nodes for the requested branch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LanguageFamilyTreeBranchResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600","schema":{"type":"string"}}}},"400":{"description":"Malformed id, language, limit or offset, or missing/ambiguous selector","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"422":{"description":"Language is a valid ISO 639-3 code but not a language of this family","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/keys":{"get":{"summary":"List the caller's own API keys","description":"Session-authenticated self-service listing (ETNI-81). Requires a Supabase session access token in the Authorization header — never an api_keys Bearer key — and returns only keys owned by that session's user. The raw key is never included; only key_prefix identifies a row.","tags":["API v2 - Keys"],"security":[{"SupabaseJwtAuth":[]}],"responses":{"200":{"description":"The caller's API keys.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKeySummary"}}}}}}},"401":{"description":"Missing or invalid session token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}},"post":{"summary":"Create a new API key for the caller","description":"Issues a public-tier key owned by the caller's session user. The raw key is returned once in the response body and is never retrievable again — only its hash and key_prefix are persisted. Partner and admin tier keys are not self-service and remain admin-issued.","tags":["API v2 - Keys"],"security":[{"SupabaseJwtAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":80}}},"example":{"label":"Script CI local"}}}},"responses":{"201":{"description":"The key was created; the raw key is shown only in this response.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyCreated"}}}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"401":{"description":"Missing or invalid session token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/keys/issue":{"post":{"summary":"Issue a public read-only API key","description":"Issues a shared, IP-bound, read-only public API key.\nOne key per IP address — returns 409 if a key already exists for this IP.\nThe address is the one the reverse proxy observed, never a value the\ncaller supplies in X-Forwarded-For.\nAttempts are limited per address (5 an hour), separately from the\nanonymous per-minute quota.\nPublic keys are rate-limited and can only perform read operations.\nPartner and admin keys must be requested via the admin UI.\nNo authentication required for this endpoint. POST only: a GET that\ncreates a key can be triggered by a crawler, a link preview or a\ncross-site request, so the former GET alias was removed.\n","tags":["API v2 - Keys"],"security":[],"responses":{"201":{"description":"Public key issued successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyIssueEnvelope"}}}},"400":{"description":"The client address could not be determined","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"409":{"description":"A public key already exists for this IP address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"429":{"description":"Too many issuance attempts from this address"},"500":{"description":"Failed to issue key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"503":{"description":"The attempt counter is unavailable; retry shortly"}}}},"/api/v2/keys/{id}":{"delete":{"summary":"Revoke one of the caller's own API keys","description":"Session-authenticated self-service revocation (ETNI-81). Sets active=false and revoked_at on a key owned by the caller's session user; already-revoked or foreign key ids both answer 404, so a probe cannot distinguish \"not yours\" from \"does not exist\".","tags":["API v2 - Keys"],"security":[{"SupabaseJwtAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"API key UUID.","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"200":{"description":"The key was revoked."},"401":{"description":"Missing or invalid session token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"No key with this id is owned by the caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/flags":{"get":{"summary":"List public editorial flags","description":"Returns flags in reverse chronological order using an opaque cursor. Responses are mutable and use Cache-Control no-store.","tags":["API v2 - Flags"],"security":[],"parameters":[{"in":"query","name":"status","schema":{"type":"string","enum":["open","under_review","accepted","rejected","withdrawn","duplicate"]},"description":"Filter by workflow status."},{"in":"query","name":"kind","schema":{"type":"string","enum":["inaccurate","missing-source","broken-url","offensive","correction-proposal","other","contribution"]},"description":"Filter by flag kind."},{"in":"query","name":"target_type","schema":{"type":"string"},"example":"people","description":"Filter by AFRIK entity type."},{"in":"query","name":"cursor","schema":{"type":"string"},"example":"MjAyNi0wNy0yNFQxMDoxNTozMC4wMDBafDljODFjYTBk","description":"Opaque cursor returned in meta.pagination.next_cursor."},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Cursor-paginated public flags.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagListResponse"},"example":{"data":[{"id":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd","public_slug":"flag-7kq3m2","target_type":"people","target_id":"PPL_YORUBA","target_field_path":"demographics.population","assertion_id":null,"flag_kind":"inaccurate","reason_text":"Population figure appears outdated compared with the latest census.","counter_source_url":"https://example.org/census/2024","counter_source_citation":"National Statistics Office, 2024 census, table 12.","proposed_rewrite":"Update the population figure using the 2024 census.","contributor_id":"bdbb6b42-3890-4c80-ac96-f732908d17c7","severity":null,"auto_generated":false,"status":"open","created_at":"2026-07-24T10:15:30.000Z","updated_at":"2026-07-24T11:00:00.000Z","resolved_at":null}],"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com","pagination":{"limit":20,"next_cursor":"MjAyNi0wNy0yNFQxMDoxNTozMC4wMDBafDljODFjYTBk"}},"errors":[]}}}},"400":{"description":"Invalid filter or cursor.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"VALIDATION_ERROR","message":"Invalid cursor","field":"cursor"}]}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"INTERNAL_ERROR","message":"Internal server error"}]}}}}}},"post":{"summary":"Submit a report on an AFRIK entity","description":"Anyone may submit a report for editorial review, subject to anti-bot verification. No account is required or offered. An optional reporter_email buys the reader the moderation decision by e-mail and nothing else: the report is created and published whether or not one is supplied, a single-use link confirms the address, and only a confirmed address is ever written to. The request language is stored to localize verification and resolution e-mails; missing or invalid values default to French. The address is never published and never returned. Responses are mutable and use Cache-Control no-store.","tags":["API v2 - Flags"],"security":[{"SupabaseJwtAuth":[]},{}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagCreateInput"},"example":{"target_type":"people","target_id":"PPL_YORUBA","target_field_path":"demographics.population","flag_kind":"inaccurate","reason_text":"Population figure appears outdated compared with the latest census.","counter_source_url":"https://example.org/census/2024","counter_source_citation":"National Statistics Office, 2024 census, table 12.","proposed_rewrite":"Update the population figure using the 2024 census.","language":"fr","antibot":{"salt":"9f2c1ab4d7e60358","nonce":"418209","difficultyBits":20,"expiresAt":1788080000000,"signature":"3b1f…"},"elapsedMs":18400}}}},"responses":{"201":{"description":"Flag created successfully.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagCreatedResponse"},"example":{"data":{"id":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd","public_slug":"flag-7kq3m2","status":"open","created_at":"2026-07-24T10:15:30.000Z"},"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[]}}}},"400":{"description":"Invalid request body.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"VALIDATION_ERROR","message":"String must contain at least 10 character(s)","field":"reason_text"}]}}}},"403":{"description":"Anti-bot verification failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"examples":{"unauthorized":{"summary":"The anti-bot proof was missing, invalid or already spent","value":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"UNAUTHORIZED","message":"vérification anti-bot échouée"}]}}}}}},"429":{"description":"Contributor flag-submission rate limit exceeded.","headers":{"Retry-After":{"schema":{"type":"integer","example":3600}},"X-RateLimit-Limit":{"schema":{"type":"integer","example":5}},"X-RateLimit-Remaining":{"schema":{"type":"integer","example":0}},"X-RateLimit-Reset":{"schema":{"type":"integer","example":1784887200}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"RATE_LIMITED","message":"Flag submission rate limit exceeded. Retry after 3600 seconds."}]}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"INTERNAL_ERROR","message":"Internal server error"}]}}}},"503":{"description":"Anti-bot verification is temporarily unavailable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"UNAVAILABLE","message":"vérification anti-bot temporairement indisponible, veuillez réessayer plus tard"}]}}}}}}},"/api/v2/flags/{public_slug_or_id}":{"get":{"summary":"Get a public editorial flag","description":"Returns a flag by UUID or public slug. Responses are mutable and use Cache-Control no-store.","tags":["API v2 - Flags"],"security":[],"parameters":[{"in":"path","name":"public_slug_or_id","required":true,"schema":{"type":"string"},"example":"flag-7kq3m2","description":"Flag UUID or public slug."}],"responses":{"200":{"description":"Public flag detail.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagDetailResponse"},"example":{"data":{"id":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd","public_slug":"flag-7kq3m2","target_type":"people","target_id":"PPL_YORUBA","target_field_path":"demographics.population","assertion_id":null,"flag_kind":"inaccurate","reason_text":"Population figure appears outdated compared with the latest census.","counter_source_url":"https://example.org/census/2024","counter_source_citation":"National Statistics Office, 2024 census, table 12.","proposed_rewrite":"Update the population figure using the 2024 census.","contributor_id":"bdbb6b42-3890-4c80-ac96-f732908d17c7","severity":null,"auto_generated":false,"status":"open","created_at":"2026-07-24T10:15:30.000Z","updated_at":"2026-07-24T11:00:00.000Z","resolved_at":null},"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[]}}}},"400":{"description":"Invalid empty identifier.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"VALIDATION_ERROR","message":"String must contain at least 1 character(s)","field":"identifier"}]}}}},"404":{"description":"No flag matches the UUID or public slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"NOT_FOUND","message":"Flag not found"}]}}}},"500":{"description":"Internal server error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"},"example":{"data":null,"meta":{"license":"CC-BY-SA-4.0","attribution":"EthniAfrica — ethniafrica.com"},"errors":[{"code":"INTERNAL_ERROR","message":"Internal server error"}]}}}}}},"patch":{"summary":"Move a report through the moderation state machine","description":"Drives one report through the transitions Postgres allows: open → under_review, and under_review → accepted | rejected | duplicate. Terminal states have no exit. Requires a bearer token whose account holds a moderator role; every applied transition writes an audit_log entry. See docs/design/moderation-charter.md §4.","tags":["API v2 - Flags"],"security":[{"SupabaseJwtAuth":[]}],"parameters":[{"in":"path","name":"public_slug_or_id","required":true,"schema":{"type":"string"},"example":"flag-7kq3m2","description":"Flag UUID or public slug."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["open","under_review","accepted","rejected","withdrawn","duplicate"]},"moderator_notes":{"type":"string","maxLength":5000}}}}}},"responses":{"200":{"description":"The updated report."},"400":{"description":"The requested status is not part of the vocabulary."},"403":{"description":"The caller holds no moderator role."},"404":{"description":"No report carries this identifier."},"409":{"description":"The state machine refuses this move from the current state."},"500":{"description":"Internal error."}}}},"/api/v2/flags/{public_slug_or_id}/audit":{"get":{"summary":"Read the recorded audit trail of one report","description":"Returns every recorded transition of one report, oldest first, for the moderation console's case file. Each entry names the **role** that acted and the level it was authorised at — never the person, their id or their address: a register has to establish that a decision was taken and at what level of authorisation, and nothing more. Requires a bearer session token whose address is on the moderation allowlist; a signed-in reader is refused. See docs/design/moderation-charter.md §4.","tags":["API v2 - Flags"],"security":[{"SupabaseJwtAuth":[]}],"parameters":[{"in":"path","name":"public_slug_or_id","required":true,"schema":{"type":"string"},"example":"00EZK83QDV","description":"Flag UUID or public slug."}],"responses":{"200":{"description":"The recorded trail.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlagAuditTrailResponse"}}}},"401":{"description":"No bearer session token."},"403":{"description":"The token's address is not on the moderation allowlist."},"404":{"description":"No report carries this identifier."},"500":{"description":"Internal error."}}}},"/api/v2/feed/revisions":{"get":{"summary":"Cursor-paginated feed of recent revisions","description":"Returns a cursor-paginated Atom + JSON feed of recent published revisions, filterable by `since`. Enables alerting and mirroring integrations to discover updated fiches without polling every endpoint (FR38 preparation, AR19, NFR32). Response body is byte-identical across replays for the same cursor (NFR32). Cache-Control: s-maxage=60 for edge caching (AR18).\n","tags":["API v2 - Feed"],"parameters":[{"in":"query","name":"since","schema":{"type":"string","format":"date-time"},"description":"ISO 8601 datetime — only return revisions published at or after this timestamp","example":"2026-05-01T00:00:00.000Z"},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of revisions per page"},{"in":"query","name":"cursor","schema":{"type":"string"},"description":"Opaque cursor from the previous page's next_cursor value"},{"in":"query","name":"format","schema":{"type":"string","enum":["json","atom"],"default":"json"},"description":"Response format — json (default) or atom (Atom 1.0 feed)"}],"responses":{"200":{"description":"Feed of revisions. JSON envelope when format=json; Atom 1.0 XML when format=atom (Content-Type: application/atom+xml).\n","headers":{"Cache-Control":{"description":"s-maxage=60, stale-while-revalidate=30","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedRevisionListResponse"}},"application/atom+xml":{"schema":{"type":"string","description":"Valid Atom 1.0 feed with one entry per revision"}}}},"400":{"description":"Invalid parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (> 60 req/min for anonymous clients)","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/dossiers":{"get":{"summary":"List the published dossiers","description":"Returns one summary per dossier, newest first. A dossier is the long-form editorial entity of the corpus: each of its chapters carries an authoritative reading of a fact and a counter-reading of the same fact, both cited. Fetch a single dossier for the chapters themselves.\n","tags":["API v2 - Dossiers"],"parameters":[{"in":"query","name":"vertical","required":false,"schema":{"type":"string","enum":["realites","nommer","histoires","spiritualites"]},"description":"Restrict the listing to one editorial vertical"}],"responses":{"200":{"description":"Dossier index envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DossierIndexEnvelope"}}},"headers":{"Cache-Control":{"description":"Shared cache duration","schema":{"type":"string","example":"s-maxage=3600"}}}},"400":{"description":"Unknown vertical","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/dossiers/{id}":{"get":{"summary":"Get one dossier by identifier","description":"Returns the whole dossier — thesis, chapters, the two readings each chapter carries, its illustration credits, its sources and the gaps it declares. Every chapter published here holds both an authoritative and a counter reading; the schema refuses to serve one that does not.\n","tags":["API v2 - Dossiers"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","pattern":"^DOS_[A-Z0-9_]+$"},"description":"Dossier identifier","example":"DOS_PROPORTIONS"}],"responses":{"200":{"description":"Dossier detail envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DossierDetailEnvelope"}}},"headers":{"Cache-Control":{"description":"Shared cache duration","schema":{"type":"string","example":"s-maxage=3600"}}}},"400":{"description":"Invalid dossier identifier format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Dossier not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/doctrine":{"get":{"summary":"List current editorial doctrine","description":"Returns the current version of every doctrine slug. Takes no query parameters.","tags":["API v2 - Source Transparency"],"responses":{"200":{"description":"Doctrine envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DoctrineResponse"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/countries":{"get":{"summary":"Liste des pays (paginée)","description":"Retourne la liste paginée de tous les pays d'Afrique avec leurs données AFRIK","tags":["API v2 - Countries"],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1,"default":1},"description":"Numéro de page","example":1},{"in":"query","name":"perPage","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Nombre d'éléments par page (max 100)","example":20}],"responses":{"200":{"description":"Liste paginée des pays","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountriesListEnvelope"}}}},"500":{"description":"Erreur serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/countries/{iso}":{"get":{"summary":"Détails d'un pays","description":"Retourne les détails complets d'un pays par son code ISO 3166-1 alpha-3, et le bloc `patronymes` — les noms attestés dans ce pays et, séparément, ceux que portent ses peuples sans y être attestés. Les deux listes affirment des choses différentes et ne sont jamais additionnées.\n","tags":["API v2 - Countries"],"parameters":[{"in":"path","name":"iso","required":true,"schema":{"type":"string","pattern":"^[A-Z]{3}$"},"description":"Code ISO 3166-1 alpha-3 du pays","example":"ZWE"}],"responses":{"200":{"description":"Détails du pays","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountryDetailEnvelope"}}}},"400":{"description":"Format de code ISO invalide","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"Pays non trouvé","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Erreur serveur","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/confidence/{entityType}/{entityId}":{"get":{"summary":"Get confidence metadata for a fiche","description":"Returns the pre-computed confidence record for a people or language-family fiche.","tags":["API v2 - Source Transparency"],"parameters":[{"in":"path","name":"entityType","required":true,"schema":{"type":"string","enum":["people","language-family"]},"example":"people"},{"in":"path","name":"entityId","required":true,"schema":{"type":"string","pattern":"^(PPL_[A-Z0-9_]+|FLG_[A-Z0-9_]+)$"},"example":"PPL_SHONA"}],"responses":{"200":{"description":"Confidence envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfidenceResponse"}}}},"400":{"description":"Invalid params","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"No confidence record found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/confidence/{entityType}/{entityId}/census":{"get":{"summary":"Count a fiche's assertions by the standing of their sources","description":"Returns the per-standing census the provenance banner states. Never an aggregate score.","tags":["API v2 - Source Transparency"],"parameters":[{"in":"path","name":"entityType","required":true,"schema":{"type":"string","enum":["people","country","language","language-family"]},"example":"country"},{"in":"path","name":"entityId","required":true,"schema":{"type":"string"},"example":"CIV"}],"responses":{"200":{"description":"Provenance census envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProvenanceCensusResponse"}}}},"400":{"description":"Invalid params","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/compare":{"get":{"summary":"Compare 2–3 entities of the same type","description":"Public comparison endpoint (Module #0 envelope) reusing the same assembly path as the SSR comparison page. Sections absent from a given entity are returned as explicit `null` (not omitted).\n","tags":["API v2 - Compare"],"parameters":[{"in":"query","name":"type","required":true,"schema":{"type":"string","enum":["peoples","countries","language-families"]},"description":"Entity type being compared","example":"peoples"},{"in":"query","name":"ids","required":true,"schema":{"type":"string"},"description":"2–3 comma-separated, distinct ids of the given type","example":"PPL_SHONA,PPL_ZULU"}],"responses":{"200":{"description":"Comparison envelope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareResponse"}}},"headers":{"Cache-Control":{"description":"s-maxage=3600","schema":{"type":"string"}}}},"400":{"description":"Invalid type, id count, or duplicate ids","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"404":{"description":"One of the ids does not exist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}},"429":{"description":"Rate limit exceeded (AR11)","headers":{"Retry-After":{"schema":{"type":"integer"}},"X-RateLimit-Limit":{"schema":{"type":"integer"}},"X-RateLimit-Remaining":{"schema":{"type":"integer"}},"X-RateLimit-Reset":{"schema":{"type":"integer"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorEnvelope"}}}}}}},"/api/v2/antibot/challenge":{"get":{"summary":"Obtain an anti-bot challenge","description":"Issues a signed proof-of-work challenge for a report submission. The caller's browser searches for a nonce whose SHA-256 of `salt + nonce` begins with `difficultyBits` zero bits, and sends the result as `antibot` on POST /api/v2/flags. Open to anyone: issuing a challenge proves nothing, the cost lies in solving it. Each challenge is single-use and short-lived. Replaces the third-party check that stood here — no visitor data leaves the service. See docs/design/moderation-charter.md §2.","tags":["API v2 - Flags"],"security":[],"responses":{"200":{"description":"A challenge to solve.","headers":{"Cache-Control":{"schema":{"type":"string","example":"no-store"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["salt","difficultyBits","expiresAt","signature"],"properties":{"salt":{"type":"string","description":"Random per-challenge value. Carries no link to a person."},"difficultyBits":{"type":"integer","description":"Required leading zero bits. Signed, so it cannot be lowered in transit."},"expiresAt":{"type":"integer","description":"Epoch milliseconds after which the challenge is refused."},"signature":{"type":"string","description":"HMAC-SHA256 over salt, difficulty and expiry."}}}}}}}},"503":{"description":"The challenge store is unreachable."}}}},"/api/v2/admin/source-tier-rulings":{"post":{"summary":"Record a moderator's decision on a citation awaiting review","description":"Records a draft ruling on one corpus citation still marked `needs_review`, named by its exact title and url. A draft publishes nothing: the fiche keeps its standing until the draft is pulled into the git ruling ledger (docs/editorial/source-review/source-tier-rulings.json) and applied to the fiches. `tier` states official, referenced or unverified — never needs_review — and is required for `tier` and `repair`, absent for `remove`; `repaired_url` belongs to a repair only. Requires a bearer session token whose address is on the moderation allowlist; every recorded draft writes an audit_log entry.","tags":["API v2 - Source Transparency"],"security":[{"SupabaseJwtAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fiche_path","source_title","source_url","decision","rationale"],"properties":{"fiche_path":{"type":"string","example":"pays/BEN.json"},"source_title":{"type":"string","example":"ONU – World Population Prospects 2025"},"source_url":{"type":["string","null"]},"decision":{"type":"string","enum":["tier","repair","remove"]},"tier":{"type":["string","null"],"enum":["official","referenced","unverified",null]},"repaired_url":{"type":["string","null"],"format":"uri"},"rationale":{"type":"string","minLength":1,"maxLength":5000}}}}}},"responses":{"201":{"description":"The recorded draft."},"400":{"description":"The decision is malformed — no rationale, a missing or forbidden tier, a repair with no corrected address."},"401":{"description":"No bearer session token."},"403":{"description":"The token's address is not on the moderation allowlist."},"500":{"description":"Internal error."}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"Optional. API key issued via /api/v2/keys/issue (public tier) or the admin UI (partner/admin tiers). Omit it to use the anonymous tier (60 requests per minute per client IP); pass it as Authorization: Bearer <key> to use its own tier's quota. A present but invalid key is refused with 401."},"SupabaseJwtAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Supabase access token for a signed-in session. Pass as Authorization: Bearer <JWT>. A session alone does not open moderator operations: reference-library writes and report transitions also require the address to be on the moderator allowlist."}},"schemas":{"PaginationMeta":{"type":"object","properties":{"total":{"type":"number","example":100},"page":{"type":"number","example":1},"perPage":{"type":"number","example":20},"totalPages":{"type":"number","example":5},"unclassifiedPeoplesCount":{"type":"number","description":"Peoples not reachable through any returnable family, surfaced instead of silently omitted (REQ-108). Only populated on the language-families list.","example":64}}},"ApiResponseV2":{"type":"object","properties":{"data":{"type":"object","description":"Données de la réponse"},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"SearchResult":{"type":"object","properties":{"type":{"type":"string","enum":["country","people","language","languageFamily"],"example":"people"},"id":{"type":"string","example":"PPL_SHONA"},"name":{"type":"string","example":"Shona"},"snippet":{"type":"string","example":"Extrait du contenu..."},"relevance":{"type":"number","example":0.95}}},"SearchNamingFormV2":{"type":"object","description":"One legacy-compatible name form. A qualifier is only returned when the corpus records one; it is never parsed from the form.","properties":{"form":{"type":"string","example":"Peul"},"qualifier":{"type":"string"},"attestedIn":{"type":"array","items":{"type":"string"},"description":"Country identifiers where this form is attested, when the name record carries them."}},"required":["form"]},"SearchNamingEraV2":{"type":"object","properties":{"era":{"type":"string","enum":["formerNames","antiquity","middleAges","precolonial","colonization","contemporary"]},"text":{"type":"string"}},"required":["era","text"]},"SearchNamingOriginFactV2":{"type":"object","description":"Structured origin facts retained only when safe for the reader-facing search surface.","properties":{"languageCode":{"type":"string"},"meaning":{"type":"string"},"imposedBy":{"type":"string"},"period":{"type":"string"}}},"SearchNamingEvidenceSourceV2":{"type":"object","description":"A hydrated source for a naming assertion. Source identifiers without these details are not exposed as evidence.","properties":{"id":{"type":"string"},"title":{"type":"string"},"author":{"type":"string"},"year":{"type":"integer"},"page":{"type":"string"},"url":{"type":"string","format":"uri"},"tier":{"type":"string","enum":["official","referenced","unverified","needs_review"]},"reviewedNarrative":{"type":"boolean"},"bibliographyNumber":{"type":"integer"},"brokenAt":{"type":["string","null"],"format":"date-time"},"citation":{"type":"string"}},"required":["id","title","tier"]},"SearchNamingEvidenceAssertionV2":{"type":"object","properties":{"id":{"type":"string"},"statement":{"type":"string"},"position":{"type":"string"},"fieldPath":{"type":"string"},"confidenceScore":{"type":"number","minimum":0,"maximum":1,"description":"Computed confidence when the entity class has a confidence record. Its absence means unmeasured, never zero."},"sourceCount":{"type":"integer","minimum":1},"lastHumanAuditAt":{"type":["string","null"],"format":"date-time"}},"required":["statement","sourceCount","lastHumanAuditAt"]},"SearchNamingEvidenceV2":{"type":"object","description":"Complete evidence for one naming assertion: the assertion and every resolved cited source travel together.","properties":{"assertion":{"$ref":"#/components/schemas/SearchNamingEvidenceAssertionV2"},"sources":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/SearchNamingEvidenceSourceV2"}},"standing":{"type":"string","enum":["official","referenced","unverified","needs_review"]}},"required":["assertion","sources","standing"]},"SearchNamingPresentationFormV2":{"type":"object","description":"One name form as the search feed can present it, without deriving facts the corpus did not record.","properties":{"form":{"type":"string"},"selfGiven":{"type":["boolean","null"],"description":"True or false only when the corpus classifies the form on this axis; null means it does not."},"qualifier":{"type":"string"},"origin":{"$ref":"#/components/schemas/SearchNamingOriginFactV2"},"attestationPeriod":{"type":"string"},"attestations":{"type":"array","items":{"type":"string"}},"problematic":{"type":"string","enum":["recorded"]},"currentUsage":{"type":"string","enum":["recorded"]},"claimStatus":{"type":"string","enum":["established","claimed","contested"]},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingEvidenceV2"}}},"required":["form","selfGiven","attestations","evidence"]},"SearchNamingPositionV2":{"type":"object","properties":{"statement":{"type":"string"},"claimStatus":{"type":"string","enum":["established","claimed","contested"]},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingEvidenceV2"}}},"required":["claimStatus","evidence"]},"SearchNamingDisagreementV2":{"type":"object","properties":{"positions":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/SearchNamingPositionV2"}}},"required":["positions"]},"SearchNamingPresentationEraV2":{"type":"object","description":"A recorded era marker. The feed receives no historical prose through this reader-facing projection.","properties":{"era":{"type":"string","enum":["formerNames","antiquity","middleAges","precolonial","colonization","contemporary"]}},"required":["era"]},"SearchNamingPresentationV2":{"type":"object","description":"Reader-facing structured naming facts. Missing facts are omitted rather than inferred or rendered as empty prose.","properties":{"forms":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingPresentationFormV2"}},"eras":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingPresentationEraV2"}},"disagreements":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingDisagreementV2"}},"origin":{"type":"string","enum":["recorded"]},"problematic":{"type":"string","enum":["recorded"]},"currentUsage":{"type":"string","enum":["recorded"]},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingEvidenceV2"}}},"required":["forms","eras","disagreements","evidence"]},"SearchNamingProjectionV2":{"type":"object","description":"The server-built naming projection shared by peoples, countries, language families, names and languages in search results.","properties":{"selfGiven":{"type":"string"},"forms":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingFormV2"}},"origin":{"type":"string"},"problem":{"type":"string"},"usageToday":{"type":"string"},"eras":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingEraV2"}},"presentation":{"$ref":"#/components/schemas/SearchNamingPresentationV2"}},"required":["forms","eras","presentation"]},"SearchAnswerV2":{"type":"object","description":"The six-block answer for one search row. An optional block whose source the fiche leaves empty is omitted, never returned empty. Text comes from the fiche (`lead`, `question`, accounts); where the fiche writes none, the client composes the sentence from `next.template` and its params. Absent on rows from servers that predate it.","properties":{"kind":{"type":"string","enum":["people","country","language","languageFamily","patronyme","word"]},"title":{"type":"string"},"what":{"type":"object","properties":{"lead":{"type":"string","description":"Written by the fiche (`content.searchAnswer.lead`, 220 characters at most). Absent: the client uses its sentence template."},"facts":{"type":"object","properties":{"population":{"type":"integer"},"countryCount":{"type":"integer"},"familyId":{"type":"string"},"peopleCount":{"type":"integer"}}}},"required":["facts"]},"origin":{"type":"object","description":"Where the name comes from, one account per claim with its own evidence. `debated` is true when accounts are contested or the fiche declares the origin debated; the page then shows every account's first sentence side by side.","properties":{"accounts":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"attribution":{"type":"string","enum":["oral","written","linguistic","synthesis"]},"claimStatus":{"type":"string","enum":["established","claimed","contested"]},"evidence":{"type":"array","items":{"$ref":"#/components/schemas/SearchNamingEvidenceV2"}}},"required":["text","evidence"]}},"debated":{"type":"boolean"}},"required":["accounts","debated"]},"names":{"type":"array","items":{"type":"object","properties":{"form":{"type":"string"},"selfGiven":{"type":["boolean","null"],"description":"Null when the fiche does not classify the form."},"shortLine":{"type":"string"},"period":{"type":"string"},"attestedIn":{"type":"array","items":{"type":"string"}}},"required":["form","selfGiven"]}},"where":{"type":"object","description":"`percent` rows are keyed by people (`peopleId`); every other unit by country (`countryId`). `speakers` is an estimate the fiche declares, in persons, never a sum of peoples' populations. `presence` (patronymes) has a null value.","properties":{"unit":{"type":"string","enum":["population","speakers","percent","presence"]},"estimate":{"type":"boolean"},"rows":{"type":"array","items":{"type":"object","properties":{"countryId":{"type":"string"},"peopleId":{"type":"string"},"value":{"type":["number","null"]}}}},"unsplitPercent":{"type":"number"},"documentedPeopleCount":{"type":"integer"}},"required":["unit","estimate","rows"]},"next":{"type":"object","description":"Either the fiche's own question, or the name of a template and its parameters.","properties":{"question":{"type":"string"},"template":{"type":"string","enum":["migration","formerName","distributionGap"]},"params":{"type":"object","additionalProperties":{"oneOf":[{"type":"string"},{"type":"number"}]}}}},"sources":{"type":"object","properties":{"count":{"type":"integer","description":"Distinct sources across every account's evidence."}},"required":["count"]}},"required":["kind","title","what","names","sources"]},"SearchResponseData":{"type":"object","description":"Search result data for one of two exclusive modes. Without `lens`, `results` is the canonical cross-kind ranking for the five mergeable non-quiz kinds: every hit, best first, each row carrying a `kind` discriminator; languages remain a grouped facet, and quiz questions are neither queried nor returned. With `lens=quiz`, only quiz questions are queried and returned, in both `quizzes` and `results`, while every non-quiz array and total is empty or zero. In the main stream, each entity kind is ALSO returned in its own array, already ordered — an exact name match first (accent- and case-insensitive), then ts_rank over the weighted search_vector (migration 043: A = name and autonym, B = exonyms, C/D = prose) OR the accent-insensitive name_unaccent_vector, both matched with a prefix operator on the last word of q (migration 052, REQ-129), multiplied for peoples by a 0.5–1.0 confidence factor. `relevance` is comparable within an array and NOT between arrays: peoples are scored ts_rank × confidence, countries by bare ts_rank, families by a match tier, persons and patronymes by the same prefix/unaccent/trigram ranking as peoples but with no confidence factor (migration 065, REQ-126; migration 066, REQ-135 — patronymes additionally fold in a dmetaphone phonetic match), languages by the same prefix/unaccent ranking with no confidence factor and an exact-match bonus that also fires on the ISO 639-3 id (migration 068, REQ-136). `normalizedScore` is the magnitude that IS comparable across kinds: migration 069 places each kind's raw relevance inside a band chosen by the match class (exact [0.90,1.00], lexical [0.50,0.90], fallback [0.00,0.50]), and it is what `results` sorts on; languages are exposed as a facet only and are not folded into `results`. A person's link to a studied people is carried by `peopleLinks[].relationLabel` (`membership` | `observation`) and is never confused with that people's own membership.","properties":{"peoples":{"type":"array","items":{"$ref":"#/components/schemas/PeopleV2"},"description":"Matching peoples, ranked. Each carries relevance, exactMatch, normalizedScore, confidence, languageFamilyName, languageFamilyNameEn (migration 084; null when the family has no English name) and a snippet whose matched terms are wrapped in [[ ]]."},"countries":{"type":"array","items":{"$ref":"#/components/schemas/CountryV2"},"description":"Matching countries, ranked by ts_rank with name_fr outranking etymology. Each carries nameEn beside nameFr, relevance, exactMatch, normalizedScore and a snippet. Empty when the request carries only a relation scope."},"families":{"type":"array","items":{"$ref":"#/components/schemas/LanguageFamilyV2"},"description":"Matching language families, ranked by afrik_search_language_families (migration 069): accent-insensitive exact (1.0) > prefix (0.6) > substring (0.3) on name_fr — then a prose tier (0.1) through search_vector for a term that appears only in the decolonial text (DEC-028). Each carries nameEn beside nameFr, relevance, exactMatch, normalizedScore and a snippet."},"persons":{"type":"array","items":{"$ref":"#/components/schemas/PersonSearchResultV2"},"description":"Matching named persons (REQ-126), ranked by afrik_search_persons (migration 065): exact full_name match, then a prefix/accent-insensitive lexical match, then a pg_trgm typo-tolerance fallback. Each carries relevance, exactMatch, normalizedScore, a snippet, and peopleLinks typing its relation to any studied/belonged-to people."},"patronymes":{"type":"array","items":{"$ref":"#/components/schemas/PatronymeSearchResultV2"},"description":"Matching names (REQ-135), ranked by afrik_search_patronymes (migration 066): exact name match, then a prefix/accent-insensitive lexical match, then a dmetaphone phonetic match, then a pg_trgm typo-tolerance fallback. Each carries relevance, exactMatch, normalizedScore and a snippet."},"quizzes":{"type":"array","items":{"$ref":"#/components/schemas/QuizSearchResultV2"},"description":"Dedicated quiz-lens page: empty without `lens` and populated only with `lens=quiz` (REQ-121). Matches active-bank questions ranked by afrik_search_quiz (migration 069) on the stem, the stimulus, the explanation and the subject entity's name. Revoked questions are invisible; the options, the correct answer and the explanation are never returned."},"results":{"type":"array","items":{"$ref":"#/components/schemas/SearchHitV2"},"description":"Canonical page for the selected mode, ordered on normalizedScore descending, ties broken on the name collated in French then on the id. Without `lens`, it merges the main stream and excludes quiz questions; with `lens=quiz`, it contains only quiz questions."},"languages":{"type":"array","items":{"$ref":"#/components/schemas/LanguageSearchResultV2"},"description":"Matching languages (REQ-136), ranked by afrik_search_languages (migration 068): exact match on the ISO 639-3 id or the name, then a prefix/accent-insensitive lexical match. Each carries nameEn, familyName, familyNameEn, relevance, exactMatch and a snippet."},"peoplesTotal":{"type":"integer","description":"Peoples matching corpus-wide, not the number returned on this page","example":16},"countriesTotal":{"type":"integer","description":"Countries matching corpus-wide","example":0},"familiesTotal":{"type":"integer","description":"Language families matching corpus-wide","example":1},"personsTotal":{"type":"integer","description":"Named persons matching corpus-wide","example":0},"patronymesTotal":{"type":"integer","description":"Names (patronymes) matching corpus-wide","example":0},"quizzesTotal":{"type":"integer","description":"Zero without `lens`; the corpus-wide count of matching active quiz questions with `lens=quiz`.","example":0},"languagesTotal":{"type":"integer","description":"Languages matching corpus-wide","example":0},"places":{"type":"array","items":{"$ref":"#/components/schemas/PlaceSearchHitV2"},"description":"Matching places (REQ-196), ranked by afrik_search_places (migration 100) on the filed name and every nameHistory name. A grouped facet like languages: not folded into `results`."},"placesTotal":{"type":"integer","description":"Places matching across every page, not only this one","example":0},"words":{"type":"array","items":{"$ref":"#/components/schemas/WordSearchHitV2"},"description":"Matching word fiches (REQ-196), ranked by afrik_search_words (migration 104) on the filed name and every nameHistory name. Each row carries the six-block `answer` of kind `word`, read from its nameHistory. A grouped facet like places: not folded into `results`."},"wordsTotal":{"type":"integer","description":"Word fiches matching across every page, not only this one","example":0},"total":{"type":"integer","description":"Without `lens`, the sum of the eight non-quiz counts, each across every page. With `lens=quiz`, this equals `quizzesTotal`. Changed in 2.2.0: this used to report the size of the returned page, which made it useless for paging.","example":17},"leads":{"type":"array","items":{"$ref":"#/components/schemas/SearchLeadV2"},"description":"Near-miss leads (REQ-125), populated only when total is 0: up to 3 suggestions across peoples, countries and language families, ranked by pg_trgm similarity alone (migration 069) below the main search's own fuzzy floor. Always an empty array when total is greater than 0."},"nearNames":{"type":"array","items":{"$ref":"#/components/schemas/SearchNearNameV2"},"description":"Qualified similar names (REQ-180) for a non-empty search: up to 3 distinct peoples, countries or language families, ranked by pg_trgm similarity and excluding entities already returned by the search. Always empty for zero-result and quiz-lens searches."},"nameAnswers":{"type":"array","items":{"$ref":"#/components/schemas/NameAnswerV2"},"description":"Reviewed, sourced answers to « where does this name come from? » for the searched term (REQ-178), matched on the whole term with accents and case ignored and localized by `lang`. Resolved from the term alone, so a search with no hit can still carry one. Empty for a name nobody has reviewed and for quiz-lens searches."},"wordAnswers":{"type":"array","items":{"$ref":"#/components/schemas/WordAnswerV2"},"description":"The answer to a published word (REQ-184), read from the production registry and matched on the whole term with accents and case ignored. Carried by the envelope and not by a row, because a word answers to no fiche and may come back with no result at all. Empty for a word nobody published and for quiz-lens searches."},"nameSuggestions":{"type":"array","items":{"type":"string"},"description":"Reviewed terms a near spelling may have meant (REQ-125), offered only when `total` is 0: within one edit of the searched term, two from eight letters, and never under four letters. The query is never rewritten; a client presents these as choices. Empty otherwise."}},"required":["peoples","countries","families","persons","patronymes","quizzes","languages","results","peoplesTotal","countriesTotal","familiesTotal","personsTotal","patronymesTotal","quizzesTotal","languagesTotal","total","leads","nearNames","nameAnswers","wordAnswers","nameSuggestions"]},"NameAnswerV2":{"type":"object","description":"A reviewed answer to where a name comes from. Written by an editor, never generated per request: rival accounts stay two accounts.","properties":{"term":{"type":"string","example":"Lingala"},"subjects":{"type":"array","description":"The fiches this answer is about. Several for a term shared by a collective and its parts; shared spelling does not assert that they are related.","items":{"type":"object","properties":{"type":{"type":"string","enum":["people","language","country","languageFamily","person","patronyme"]},"id":{"type":"string"}},"required":["type","id"]}},"paragraphs":{"type":"array","minItems":1,"maxItems":2,"items":{"type":"string"}},"uncertainty":{"type":"string","description":"What the sources leave unsettled, when anything."},"sources":{"type":"array","description":"One entry per key claim: the statement and the sources behind it, in the shape the source-chain sheet reads.","items":{"type":"object"}}},"required":["term","subjects","paragraphs","sources"]},"WordAnswerV2":{"type":"object","description":"The answer to a published word (REQ-184), built from its record in the production registry. It follows the answer contract of the result page (`kind` is always `word`) and lists the queries that find it. A block with no data is an absent key.","properties":{"kind":{"type":"string","enum":["word"]},"title":{"type":"string","example":"pharaon"},"queries":{"type":"array","description":"The accent-free, lowercase queries the registry files for this word.","items":{"type":"string"}},"what":{"type":"object","properties":{"lead":{"type":"string","description":"Written in the registry's `answer.lead`; absent when none was written."},"facts":{"type":"object"}},"required":["facts"]},"origin":{"type":"object","properties":{"accounts":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"attribution":{"type":"string","enum":["oral","written","linguistic","synthesis"]},"evidence":{"type":"array","items":{"type":"object"}}},"required":["text","evidence"]}},"debated":{"type":"boolean"}},"required":["accounts","debated"]},"names":{"type":"array","description":"The word's forms, none marked as the one a people gives itself.","items":{"type":"object","properties":{"form":{"type":"string"},"selfGiven":{"type":["boolean","null"]}},"required":["form","selfGiven"]}},"next":{"type":"object","properties":{"question":{"type":"string"}}},"path":{"type":"array","description":"The form of the word in each language it passed through.","items":{"type":"object","properties":{"form":{"type":"string"},"language":{"type":"string"},"period":{"type":"string"}},"required":["form","language"]}},"publications":{"type":"array","items":{"type":"object","properties":{"network":{"type":"string"},"url":{"type":"string"},"format":{"type":"string"},"publishedAt":{"type":"string"}},"required":["network","url"]}},"sources":{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}},"required":["kind","title","queries","what","names","sources"]},"SearchLeadV2":{"type":"object","description":"A near-miss lead (REQ-125): what the search engine almost understood, only ever returned when the main search's total is 0.","properties":{"kind":{"type":"string","enum":["people","country","family"],"description":"The entity kind this lead belongs to — the three kinds the search surface names to the reader."},"id":{"type":"string","description":"The entity's own identifier."},"name":{"type":"string","description":"The entity's display name."},"similarity":{"type":"number","description":"pg_trgm similarity of the folded query against this name, in [0.2, 1].","example":0.27}},"required":["kind","id","name","similarity"]},"SearchNearNameV2":{"type":"object","description":"A name qualified by the API as similar to a non-empty search, with its pg_trgm similarity. It is a distinct entity from every returned people, country and language-family result.","properties":{"kind":{"type":"string","enum":["people","country","family"]},"id":{"type":"string"},"name":{"type":"string"},"similarity":{"type":"number","minimum":0.2,"maximum":1}},"required":["kind","id","name","similarity"]},"QuizSearchResultV2":{"type":"object","description":"A quiz-bank search hit (REQ-121). The projection is a closed list: options_fr and correct_option are the answer key and explanation_fr states the answer in prose, so all three are searched and none is returned — finding a question through search must not answer it.","properties":{"id":{"type":"string","example":"b1f0e2c4-…"},"prompt":{"type":"string","description":"The question stem, as asked.","example":"Quel est l'autonyme des Wolof ?"},"entityType":{"type":"string","description":"The subject's kind — `people` or `country`.","example":"people"},"entityId":{"type":"string","example":"PPL_WOLOF"},"subjectName":{"type":["string","null"],"description":"French name of the people or country the question is about — how a reader reaches the bank, since nobody searches the wording of a question.","example":"Wolof"},"relevance":{"type":"number","example":0.42},"exactMatch":{"type":"boolean","description":"True when the reader named the question's subject, not when they retyped the stem."},"normalizedScore":{"type":"number","minimum":0,"maximum":1,"description":"Cross-kind ranking score on [0,1] (migration 069), the magnitude `results` sorts on.","example":0.94},"snippet":{"type":["string","null"],"description":"Match excerpt over the subject, the stem and the stimulus only; matched terms are wrapped in [[ ]]. Never draws on the explanation."}},"required":["id","prompt","entityType","entityId","subjectName","relevance","exactMatch","normalizedScore","snippet"]},"SearchHitV2":{"type":"object","description":"One row of the canonical cross-kind ranking. The grouped arrays answer what a query found among peoples, or among countries; this answers what it found, best first, whatever the kind.","properties":{"kind":{"type":"string","enum":["people","country","languageFamily","person","patronyme","quiz"],"description":"Which array of this same response carries the full record for this id."},"id":{"type":"string","example":"PPL_WOLOF"},"name":{"type":"string","description":"The hit's display name in the requested locale, with French fallback — the question stem for a quiz row.","example":"Wolof"},"normalizedScore":{"type":"number","minimum":0,"maximum":1,"description":"Cross-kind ranking score on [0,1] (migration 069). The match class picks a disjoint band and the kind's own raw relevance is placed inside it, so the class always dominates and the raw magnitude — measured on a different scale per kind — only breaks ties within one class.","example":0.94},"snippet":{"type":["string","null"],"description":"Match excerpt where the kind provides one; matched terms are wrapped in [[ ]]."}},"required":["kind","id","name","normalizedScore","snippet"]},"SearchResponse":{"type":"object","description":"Module #0 envelope for /v2/search (ETNI-38)","properties":{"data":{"$ref":"#/components/schemas/SearchResponseData"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"SearchCompanionSubject":{"type":"object","description":"One typed subject used to resolve companion content. The identifier must match its type: PPL_* for people, ISO 3166-1 alpha-3 for countries, FLG_* for language families, ISO 639-3 lowercase for languages, and PAT_* for patronymes.","properties":{"entityType":{"type":"string","enum":["people","country","languageFamily","language","patronyme"]},"entityId":{"type":"string","minLength":1,"example":"PPL_BASSA"}},"required":["entityType","entityId"]},"SearchCompanionWordMatch":{"type":"object","description":"A short found by the word the reader typed, not by an entity: there is no subject to name, so the match carries the word itself, lowercased and without accents.","properties":{"relation":{"type":"string","enum":["word"]},"word":{"type":"string","minLength":1,"example":"zombie"}},"required":["relation","word"]},"SearchCompanionMatch":{"type":"object","description":"Why this individual item is related to the requested subjects. Provenance is item-level because different shelves and items may come from different widening rings.","properties":{"relation":{"type":"string","enum":["exact","linked-family","linked-people","linked-country","recent"]},"entityType":{"type":"string","enum":["people","country","languageFamily","language","patronyme"]},"entityId":{"type":"string","minLength":1,"example":"PPL_BASSA"}},"required":["relation","entityType","entityId"]},"SearchCompanionSource":{"type":"object","properties":{"title":{"type":"string","minLength":1},"url":{"type":["string","null"],"format":"uri"},"tier":{"type":"string","enum":["official","referenced","unverified"]},"notes":{"type":"string","minLength":1},"sourceKind":{"type":"string","enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","press","encyclopedia","ngo","missionary_database","community","repository","archive","discovery","ai_generated","unknown","oral_tradition","ethniafrica_synthesis"],"description":"What kind of work the source is. Absent when the source records none."}},"required":["title","url","tier"]},"SearchCompanionPoster":{"type":"object","properties":{"src":{"type":"string","minLength":1},"alt":{"type":"string","minLength":1},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}},"required":["src","alt","width","height"]},"SearchCompanionIllustration":{"type":"object","properties":{"src":{"type":"string","minLength":1},"alt":{"type":"string","minLength":1},"credit":{"type":"string","minLength":1},"licenceUrl":{"type":"string","format":"uri"},"filePage":{"type":"string","format":"uri"}},"required":["src","alt","credit"]},"SearchCompanionShort":{"type":"object","properties":{"id":{"type":"string","minLength":1},"href":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","minLength":1},"publishedAt":{"oneOf":[{"type":"string","format":"date"},{"type":"string","format":"date-time"}]},"durationSeconds":{"type":"integer","minimum":1},"watchUrl":{"type":"string","format":"uri"},"poster":{"$ref":"#/components/schemas/SearchCompanionPoster"},"source":{"$ref":"#/components/schemas/SearchCompanionSource"},"match":{"oneOf":[{"$ref":"#/components/schemas/SearchCompanionMatch"},{"$ref":"#/components/schemas/SearchCompanionWordMatch"}]}},"required":["id","href","name","description","publishedAt","durationSeconds","watchUrl","poster","source","match"]},"SearchCompanionAnecdote":{"type":"object","properties":{"id":{"type":"string","minLength":1},"contentLanguage":{"type":"string","enum":["fr"]},"headline":{"type":"string","minLength":1},"body":{"type":"array","minItems":1,"maxItems":2,"items":{"type":"string","minLength":1}},"tier":{"type":"string","enum":["official","referenced","unverified"]},"sources":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/SearchCompanionSource"}},"illustration":{"$ref":"#/components/schemas/SearchCompanionIllustration"},"match":{"$ref":"#/components/schemas/SearchCompanionMatch"}},"required":["id","contentLanguage","headline","body","tier","sources","illustration","match"]},"SearchCompanionProverbOriginal":{"type":"object","properties":{"text":{"type":"string","minLength":1},"lang":{"type":"string","minLength":2},"language":{"type":"string","minLength":1}},"required":["text","lang","language"]},"SearchCompanionProverb":{"type":"object","properties":{"id":{"type":"string","minLength":1},"contentLanguage":{"type":"string","enum":["fr"]},"text":{"type":"string","minLength":1},"meaning":{"type":"string","minLength":1},"original":{"oneOf":[{"$ref":"#/components/schemas/SearchCompanionProverbOriginal"},{"type":"null"}]},"origin":{"type":"object","properties":{"status":{"type":"string","enum":["attested"]},"note":{"type":"string"}},"required":["status","note"]},"sources":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/SearchCompanionSource"}},"match":{"$ref":"#/components/schemas/SearchCompanionMatch"}},"required":["id","contentLanguage","text","meaning","original","origin","sources","match"]},"SearchCompanionQuizOption":{"oneOf":[{"type":"string"},{"type":"object","properties":{"autonym":{"type":"string"},"exonym":{"type":"string"}},"required":["autonym"]}]},"SearchCompanionQuiz":{"type":"object","properties":{"id":{"type":"string","minLength":1},"templateId":{"type":"string","enum":["T1","T2","T3","T4","T6","T7","T8","T9","T10","T11","T12","T13","T14","T15","T16","T17","T18"]},"contentLanguage":{"type":"string","enum":["fr"]},"prompt":{"type":"string","minLength":1},"stimulus":{"type":["string","null"]},"options":{"type":"array","minItems":2,"items":{"$ref":"#/components/schemas/SearchCompanionQuizOption"}},"correctOption":{"type":"integer","minimum":0,"maximum":3},"explanation":{"type":"string","minLength":1},"assertionId":{"type":"string","minLength":1},"source":{"$ref":"#/components/schemas/SearchCompanionSource"},"entity":{"type":"object","properties":{"type":{"type":"string","enum":["people","country"]},"id":{"type":"string","minLength":1}},"required":["type","id"]},"match":{"$ref":"#/components/schemas/SearchCompanionMatch"}},"required":["id","templateId","contentLanguage","prompt","stimulus","options","correctOption","explanation","assertionId","source","entity","match"]},"SearchCompanionShortSelection":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Total matching items before the response limit is applied."},"items":{"type":"array","maxItems":6,"items":{"$ref":"#/components/schemas/SearchCompanionShort"}}},"required":["count","items"]},"SearchCompanionAnecdoteSelection":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Total matching items before the response limit is applied."},"items":{"type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/SearchCompanionAnecdote"}}},"required":["count","items"]},"SearchCompanionProverbSelection":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Total matching items before the response limit is applied."},"items":{"type":"array","maxItems":2,"items":{"$ref":"#/components/schemas/SearchCompanionProverb"}}},"required":["count","items"]},"SearchCompanionQuizSelection":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"description":"Total matching quiz items before the single-item response limit is applied."},"item":{"oneOf":[{"$ref":"#/components/schemas/SearchCompanionQuiz"},{"type":"null"}]}},"required":["count","item"]},"SearchCompanionsData":{"type":"object","properties":{"subjects":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/SearchCompanionSubject"}},"shorts":{"$ref":"#/components/schemas/SearchCompanionShortSelection"},"anecdotes":{"$ref":"#/components/schemas/SearchCompanionAnecdoteSelection"},"proverbs":{"$ref":"#/components/schemas/SearchCompanionProverbSelection"},"quiz":{"$ref":"#/components/schemas/SearchCompanionQuizSelection"}},"required":["subjects","shorts","anecdotes","proverbs","quiz"]},"SearchCompanionsResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SearchCompanionsData"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"PeopleSummaryV2":{"type":"object","description":"Lightweight people reference embedded in a name record.","properties":{"id":{"type":"string","description":"Identifiant PPL_*","example":"PPL_JIENG"},"nameMain":{"type":"string","example":"Jieng"},"autonym":{"type":["string","null"],"description":"Self-appellation (endonym), when known","example":"Jieng"},"slug":{"type":"string","description":"URL slug — the PPL_* id","example":"PPL_JIENG"}},"required":["id","nameMain","autonym","slug"]},"NameRecordV2":{"type":"object","description":"A single name-variant record (endonym | exonym | historical_spelling | surname).","properties":{"id":{"type":"string","format":"uuid"},"peopleId":{"type":"string","example":"PPL_JIENG"},"nameText":{"type":"string","example":"Dinka"},"nameType":{"type":"string","enum":["endonym","exonym","historical_spelling","surname"],"example":"exonym"},"languageOfOrigin":{"type":["string","null"],"example":"din"},"meaning":{"type":["string","null"]},"periodLabel":{"type":["string","null"]},"imposedBy":{"type":["string","null"],"example":"colonial administration"},"impositionPeriod":{"type":["string","null"]},"whyProblematic":{"type":["string","null"]},"contemporaryUsage":{"type":["string","null"]},"sortRank":{"type":"integer","example":1},"people":{"oneOf":[{"$ref":"#/components/schemas/PeopleSummaryV2"},{"type":"null"}]}},"required":["id","peopleId","nameText","nameType","sortRank","people"]},"ListNamesData":{"type":"object","description":"GET /v2/names result data.","properties":{"names":{"type":"array","items":{"$ref":"#/components/schemas/NameRecordV2"}},"total":{"type":"integer","example":2}},"required":["names","total"]},"ListNamesResponse":{"type":"object","description":"Module #0 envelope for /v2/names (ETNI-471)","properties":{"data":{"$ref":"#/components/schemas/ListNamesData"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"ComparisonEntity":{"type":"object","description":"One compared entity. Section keys are the strict-model content keys for its type; a section absent from the source fiche is an explicit `null`, never omitted.","properties":{"type":{"type":"string","enum":["peuple","pays","famille"],"description":"Internal AFRIK entity type"},"id":{"type":"string","description":"PPL_*, ISO 3166-1 alpha-3, or FLG_* id","example":"PPL_SHONA"},"label":{"type":"string","example":"Shona"}},"required":["type","id","label"],"additionalProperties":{"description":"Comparable section value (nullable), keyed by content section name (e.g. appellations, origins, culture)."}},"CompareData":{"type":"object","description":"GET /v2/compare result data.","properties":{"entityType":{"type":"string","enum":["peoples","countries","language-families"]},"entities":{"type":"array","minItems":2,"maxItems":3,"items":{"$ref":"#/components/schemas/ComparisonEntity"}}},"required":["entityType","entities"]},"CompareResponse":{"type":"object","description":"Module #0 envelope for /v2/compare (ETNI-480)","properties":{"data":{"$ref":"#/components/schemas/CompareData"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"NameHistorySourceV2":{"type":"object","description":"A source of a name-history account. An oral source is never refused for having no URL or no page (REQ-195): it names what it can of its narrative, carrier and place.","properties":{"title":{"type":"string","minLength":1},"author":{"type":"string","minLength":1},"year":{"type":["integer","null"]},"url":{"type":["string","null"]},"tier":{"type":"string","enum":["official","referenced","unverified"]},"source_kind":{"type":"string","enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","press","encyclopedia","ngo","missionary_database","community","repository","archive","discovery","ai_generated","unknown","oral_tradition","ethniafrica_synthesis"]},"page":{"type":"string","minLength":1},"narrative":{"type":"string","minLength":1},"carrier":{"type":"string","minLength":1},"place":{"type":"string","minLength":1},"notes":{"type":"string"}},"required":["title","author","year","url","tier","source_kind"]},"NameHistoryAccountV2":{"type":"object","description":"One dated account of a name. Accounts sharing a hypothesisGroup are competing hypotheses, shown side by side and never ranked.","properties":{"period":{"type":"object","description":"Integer years, negative before the common era; either bound may be unknown. `label` is what the reader reads.","properties":{"from":{"type":["integer","null"]},"to":{"type":["integer","null"]},"label":{"type":"string","minLength":1}},"required":["from","to","label"]},"era":{"type":"string","enum":["polity","colonial","modern"],"description":"The great era of the territory the account is about, at its period: before colonial rule, under it, or since independence. Absent when the account does not settle where or when."},"statement":{"type":"string","minLength":1},"aspect":{"type":"string","enum":["meaning","imposition","usage"],"description":"What the account says of the name as a whole: its meaning, who imposed it and why that is a problem, or how it is used today."},"formAsWritten":{"type":"string","minLength":1,"description":"A written trace of the name: the form exactly as the cited document spells it, cited at its page."},"hypothesisGroup":{"type":"string","minLength":1},"birth":{"type":"boolean","enum":[true],"description":"The earliest origin of this name the project knows; at most one per name."},"before":{"type":"boolean","enum":[true],"description":"What existed before this name."},"actors":{"type":"array","description":"Context for the reader, never the author of a name.","items":{"type":"object","properties":{"name":{"type":"string","minLength":1},"role":{"type":"string","minLength":1},"personId":{"type":"string","minLength":1}},"required":["name","role"]}},"sources":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/NameHistorySourceV2"}}},"required":["period","statement","sources"]},"WordSearchHitV2":{"type":"object","description":"A word search hit (REQ-196): exact name match, then a prefix match on any of its names, then a pg_trgm fallback; normalizedScore is the cross-kind scale of migration 069.","properties":{"id":{"type":"string","example":"WRD_RACE"},"nameMain":{"type":"string","example":"race"},"wordLanguage":{"type":"string","example":"fra"},"definition":{"type":"string"},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"},"naming":{"$ref":"#/components/schemas/SearchNamingProjectionV2"},"answer":{"$ref":"#/components/schemas/SearchAnswerV2"},"relevance":{"type":"number"},"exactMatch":{"type":"boolean"},"normalizedScore":{"type":"number","minimum":0,"maximum":1}},"required":["id","nameMain","exactMatch","normalizedScore"]},"WordSummaryV2":{"type":"object","description":"One row of the word list (REQ-196).","properties":{"id":{"type":"string","example":"WRD_RACE"},"nameMain":{"type":"string","example":"race"},"wordLanguage":{"type":"string","example":"fra"}},"required":["id","nameMain","wordLanguage"]},"WordV2":{"type":"object","description":"A word fiche (REQ-196): a word whose history explains Africa through its names. `names` lists every name the word answers to — its filed name, then each nameText of its nameHistory — with no form promoted over another; `nameHistory`, always present for a word, tells where each comes from.","properties":{"id":{"type":"string","example":"WRD_RACE"},"nameMain":{"type":"string","example":"race"},"names":{"type":"array","items":{"type":"string"},"example":["race"]},"wordLanguage":{"type":"string","description":"ISO 639-3 code of the language of the word.","example":"fra"},"definition":{"type":"string","description":"One sentence: what the word means today."},"relatedSubjects":{"type":"array","description":"Fiches of any class the word is tied to, with the fiche's own sentence saying how.","items":{"type":"object","properties":{"id":{"type":"string","example":"FLG_MANDE"},"relation":{"type":"string"}},"required":["id","relation"]}},"gaps":{"type":"array","items":{"type":"string"}},"sources":{"type":"array","items":{"type":"object"}},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"}},"required":["id","nameMain","names","wordLanguage","definition","relatedSubjects","gaps","sources","nameHistory"]},"WordDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WordV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"WordListEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WordSummaryV2"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"NameHistoryV2":{"type":"object","description":"The subject's name history (REQ-196, ARCH-028): one shape on every subject, validated by the one shared schema before it is loaded. Absent when the fiche declares none yet.","properties":{"summary":{"type":"string","minLength":1},"selfNameUnknown":{"type":"boolean","enum":[true],"description":"No source gives the name the people uses for itself; no name is then self-given."},"names":{"type":"array","minItems":1,"items":{"type":"object","properties":{"nameText":{"type":"string","minLength":1},"nameStatus":{"type":"string","enum":["current","former"]},"selfGiven":{"type":"boolean"},"languageOfOrigin":{"type":["string","null"]},"namedBy":{"type":["string","null"]},"shortLine":{"type":"string","minLength":1,"maxLength":120,"description":"One sentence a reader understands without context."},"usedIn":{"type":"array","items":{"type":"string","minLength":1},"description":"ISO 639-3 codes of the languages the name is used in."},"pronunciation":{"type":"object","properties":{"respelling":{"type":"string","minLength":1},"audio":{"type":["object","null"],"properties":{"url":{"type":"string","minLength":1},"consent":{"type":"string","minLength":1}},"required":["url","consent"]},"source":{"$ref":"#/components/schemas/NameHistorySourceV2"}},"required":["respelling","audio","source"]},"periodLabel":{"type":"string","minLength":1,"description":"When the name is or was in use."},"variantSpelling":{"type":"boolean","enum":[true],"description":"Another spelling of a name rather than a name of its own."},"accounts":{"type":"array","items":{"$ref":"#/components/schemas/NameHistoryAccountV2"}}},"required":["nameText","nameStatus","selfGiven","languageOfOrigin","namedBy","accounts"]}}},"required":["summary","names"]},"CountryV2":{"type":"object","properties":{"id":{"type":"string","description":"Code ISO 3166-1 alpha-3","example":"ZWE"},"nameFr":{"type":"string","example":"Zimbabwe"},"nameEn":{"type":"string","description":"English name of ordinary use, in the state's own English form (Chad, Côte d'Ivoire, Cabo Verde, The Gambia). Absent until the next data reload fills migration 084's column.","example":"Zimbabwe"},"nameOfficial":{"type":"string","example":"Republic of Zimbabwe"},"etymology":{"type":"string"},"content":{"type":"object","description":"Contenu évolutif en JSONB"},"naming":{"$ref":"#/components/schemas/SearchNamingProjectionV2"},"answer":{"$ref":"#/components/schemas/SearchAnswerV2"},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"}}},"PeopleV2":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant PPL_*","example":"PPL_SHONA"},"nameMain":{"type":"string","example":"Shona"},"languageFamilyId":{"type":"string","description":"Identifiant FLG_*","example":"FLG_BANTU"},"currentCountries":{"type":"array","items":{"type":"string"},"example":["ZWE","MOZ"]},"content":{"type":"object","description":"Contenu évolutif en JSONB"},"naming":{"$ref":"#/components/schemas/SearchNamingProjectionV2"},"answer":{"$ref":"#/components/schemas/SearchAnswerV2"},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"}}},"PersonPeopleLinkV2":{"type":"object","description":"A person's typed link to a people (migration 057, REQ-126): membership (belongs to the people) or observation (studied the people — e.g. an ethnographer). Never inferred — always the value the fiche declares.","properties":{"peopleId":{"type":"string","description":"Identifiant PPL_*","example":"PPL_BAMBARA"},"relationLabel":{"type":"string","enum":["membership","observation"],"example":"observation"}},"required":["peopleId","relationLabel"]},"PersonSearchResultV2":{"type":"object","description":"A named person search hit (ARCH-018, REQ-126) — an ethnographer, author, informant, translator, historian, etc.","properties":{"id":{"type":"string","description":"Identifiant PER_*","example":"PER_MODIBO_KEITA"},"fullName":{"type":"string","example":"Modibo Keïta"},"roleCategory":{"type":"string","example":"ethnographer"},"relevance":{"type":"number","example":0.82},"exactMatch":{"type":"boolean","example":false},"normalizedScore":{"type":"number","minimum":0,"maximum":1,"description":"Cross-kind ranking score on [0,1] (migration 069), the magnitude `results` sorts on.","example":0.94},"snippet":{"type":"string","description":"Match excerpt over fullName and roleCategory; matched terms are wrapped in [[ ]]."},"peopleLinks":{"type":"array","items":{"$ref":"#/components/schemas/PersonPeopleLinkV2"},"description":"This person's typed links to studied/belonged-to peoples — never collapsed into membership."}},"required":["id","fullName","roleCategory","relevance","exactMatch","normalizedScore","snippet","peopleLinks"]},"PatronymeSearchResultV2":{"type":"object","description":"A name (patronyme) search hit (REQ-135), ranked by afrik_search_patronymes (migration 066): exact name match, then a prefix/accent-insensitive lexical match, then a dmetaphone phonetic match (bridging spellings like Keyta/Keïta), then a pg_trgm typo-tolerance fallback.","properties":{"id":{"type":"string","description":"Identifiant PATR_*","example":"PATR_KEITA"},"nameMain":{"type":"string","example":"Keïta"},"nameSystem":{"type":"string","description":"DEC-039's naming-system discriminant.","example":"patronymic"},"casteOrSocialFunction":{"type":["string","null"]},"content":{"type":"object","description":"Evolutionary JSONB content, forwarded opaquely."},"naming":{"$ref":"#/components/schemas/SearchNamingProjectionV2"},"answer":{"$ref":"#/components/schemas/SearchAnswerV2"},"associatedPeoples":{"type":"array","description":"Associated peoples whose fiche exists, resolved to their main name in fiche order — ETNI-1859. Absent when the name declares no people.","items":{"type":"object","properties":{"id":{"type":"string","example":"PPL_MANDINGUE"},"name":{"type":"string","example":"Mandingue"}},"required":["id","name"]}},"relevance":{"type":"number","example":0.82},"exactMatch":{"type":"boolean","example":false},"normalizedScore":{"type":"number","minimum":0,"maximum":1,"description":"Cross-kind ranking score on [0,1] (migration 069), the magnitude `results` sorts on.","example":0.94},"snippet":{"type":"string","description":"Match excerpt over nameMain; matched terms are wrapped in [[ ]]."}},"required":["id","nameMain","nameSystem","casteOrSocialFunction","content","relevance","exactMatch","normalizedScore","snippet"]},"LanguageSearchResultV2":{"type":"object","description":"A language search hit (REQ-136), ranked by afrik_search_languages (migration 068): exact match on the ISO 639-3 id or the accent/case-insensitive name, then a prefix/accent-insensitive lexical match over the weighted search_vector (migration 055) OR name_unaccent_vector (this migration).","properties":{"id":{"type":"string","description":"ISO 639-3 code","example":"swa"},"name":{"type":"string","example":"Swahili"},"nameEn":{"type":["string","null"],"description":"The fiche's English name (content.nameEn), carried as fiche data and not searched; null when the fiche carries none.","example":"Swahili"},"familyId":{"type":"string","description":"Identifiant FLG_*","example":"FLG_NIGER_CONGO"},"familyName":{"type":["string","null"],"example":"Niger-Congo"},"familyNameEn":{"type":["string","null"],"example":"Niger-Congo"},"content":{"type":"object","description":"Evolutionary JSONB content, forwarded opaquely."},"naming":{"$ref":"#/components/schemas/SearchNamingProjectionV2"},"answer":{"$ref":"#/components/schemas/SearchAnswerV2"},"relevance":{"type":"number","example":0.82},"exactMatch":{"type":"boolean","description":"Fires on the ISO 639-3 id as well as on the name — a reader who types \"swa\" has named the language exactly as precisely as one who types \"Swahili\".","example":false},"snippet":{"type":"string","description":"Match excerpt over name; matched terms are wrapped in [[ ]]."}},"required":["id","name","familyId","familyName","content","relevance","exactMatch","snippet"]},"LanguageFamilyV2":{"type":"object","properties":{"id":{"type":"string","description":"Identifiant FLG_*","example":"FLG_BANTU"},"nameFr":{"type":"string","example":"Bantou"},"nameEn":{"type":"string","example":"Bantu"},"peopleCount":{"type":"integer","minimum":0,"description":"Number of afrik_peoples rows carrying this family's id, computed from stored rows (REQ-108) — not the fiche-declared associatedPeoples length.","example":28},"associatedPeoples":{"type":"array","description":"Canonical associated peoples derived from afrik_peoples.language_family_id.","items":{"type":"object","required":["name","peopleId"],"properties":{"name":{"type":"string","example":"Shona"},"peopleId":{"type":"string","description":"Stable PPL_* identifier.","example":"PPL_SHONA"}}}},"content":{"type":"object","description":"Evolutionary JSONB content. Its associatedPeoples property is a legacy compatibility copy derived from the canonical top-level array."},"naming":{"$ref":"#/components/schemas/SearchNamingProjectionV2"},"answer":{"$ref":"#/components/schemas/SearchAnswerV2"},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"}}},"LanguageSourceV2":{"type":"object","properties":{"id":{"type":"string","minLength":1},"title":{"type":"string","minLength":1},"url":{"type":["string","null"]},"tier":{"type":"string","enum":["official","referenced","unverified"]},"notes":{"type":["string","null"]}},"required":["id","title","url","tier"]},"LanguageV2":{"type":"object","properties":{"id":{"type":"string","pattern":"^[a-z]{3}$","example":"yor"},"name":{"type":"string","minLength":1},"nameProvenance":{"type":"string","enum":["sourced","derived"]},"isoCode639_3":{"type":"string","pattern":"^[a-z]{3}$","example":"yor"},"glottocode":{"type":["string","null"],"example":"yoru1245"},"nameEn":{"type":["string","null"],"example":"Yoruba"},"alternateNames":{"type":"array","items":{"type":"string","minLength":1}},"spellingAliases":{"type":"array","items":{"type":"string","minLength":1}},"dialects":{"type":"array","items":{"type":"string","minLength":1}},"family":{"type":"object","properties":{"id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1}},"required":["id","name"]},"speakingPeoples":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1}},"required":["id","name"]}},"vehicularRole":{"type":["string","null"]},"vitalityStatus":{"type":["object","null"],"properties":{"status":{"type":"string","minLength":1},"scale":{"type":"string","minLength":1},"asOf":{"type":"integer","minimum":1}},"required":["status","scale","asOf"]},"sources":{"type":"array","items":{"$ref":"#/components/schemas/LanguageSourceV2"}},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"}},"required":["id","name","nameProvenance","isoCode639_3","glottocode","nameEn","alternateNames","spellingAliases","dialects","family","speakingPeoples","vehicularRole","vitalityStatus","sources"]},"PatronymeV2":{"type":"object","description":"A name (patronyme) — DEC-038's fifth dimension of the atlas. Bearer entries are a narrow allow-listed summary; no code path takes a family name and returns an ethnic origin for a named living person (DEC-040).","properties":{"id":{"type":"string","pattern":"^PAT_[A-Z0-9_]+$","example":"PAT_KEITA"},"nameMain":{"type":"string","minLength":1},"nameSystem":{"type":"string","enum":["clan_name","non_hereditary_patronymic","nisba","praise_name","totemic_clan"]},"casteOrSocialFunction":{"type":["string","null"]},"nameHistory":{"$ref":"#/components/schemas/NameHistoryV2"},"content":{"type":"object","description":"Opaque passthrough of the name's remaining DEC-039 fields, forwarded as-is pending ETNI-1460's strict per-subtype shape."},"associatedPeoples":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"nameMain":{"type":"string","minLength":1},"autonym":{"type":["string","null"]},"slug":{"type":"string","minLength":1}},"required":["id","nameMain","autonym","slug"]}},"associatedCountries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"nameFr":{"type":"string","minLength":1}},"required":["id","nameFr"]}},"bearers":{"type":"array","description":"Narrow allow-listed summary — id, fullName, roleCategory only (DEC-040).","items":{"type":"object","properties":{"id":{"type":"string","minLength":1},"fullName":{"type":"string","minLength":1},"roleCategory":{"type":"string","minLength":1}},"required":["id","fullName","roleCategory"]}},"namedBearers":{"type":"array","description":"Bearers the atlas can only name, because no person record exists for them — they carry a name and a status, never an id, a role or a biography, which is why they are a separate list and not a `bearers` entry with empty fields. Only a bearer the atlas records as dead is served: a family name is an ethnic marker, so publishing a living one would publish their ethnic origin (DEC-040, RGPD art. 9).","items":{"type":"object","properties":{"displayName":{"type":"string","minLength":1,"example":"Arisekola Alao"},"status":{"type":"string","enum":["deceased"]}},"required":["displayName","status"]}},"alliances":{"type":"array","description":"Names this name is allied with (joking-kinship pacts such as sanankuya), in the dossier's own order. The target is resolved to its name so a client never has to print a raw PAT_ id; allianceType is the attested term, or null when the dossier records the pact without naming it.","items":{"type":"object","properties":{"targetId":{"type":"string","pattern":"^PAT_[A-Z0-9_]+$","example":"PAT_COULIBALY"},"targetNameMain":{"type":"string","example":"Coulibaly"},"allianceType":{"type":["string","null"],"example":"sanankuya"}},"required":["targetId","targetNameMain","allianceType"]}}},"required":["id","nameMain","nameSystem","casteOrSocialFunction","content","associatedPeoples","associatedCountries","bearers","namedBearers","alliances"]},"Error":{"type":"object","properties":{"error":{"type":"string","example":"Resource not found"}}},"ApiResponseMeta":{"type":"object","description":"Envelope meta block for Module #0 responses. Always carries license + attribution (AR8). Optionally includes pagination, confidence score, and pinned-version URL.","properties":{"license":{"type":"string","example":"CC-BY-SA-4.0"},"attribution":{"type":"string","example":"EthniAfrica — ethniafrica.com"},"confidence":{"type":["number","null"],"example":73,"description":"Score 0–100 if applicable"},"pinned_url":{"type":["string","null"],"example":"https://ethniafrica.com/peuples/yoruba@v4"},"pagination":{"$ref":"#/components/schemas/PaginationMeta"},"translation":{"oneOf":[{"$ref":"#/components/schemas/TranslationProvenance"},{"type":"null"}],"description":"No longer emitted: translation of the fiches was retired and every record is served as authored, in French. Kept declared so existing clients are not broken."}},"required":["license","attribution"]},"TranslationProvenance":{"type":"object","description":"How the served record was translated (REQ-142). A separate axis from source tier and source_kind; never alters the confidence score.","properties":{"kind":{"type":"string","enum":["human","machine_reviewed","machine"]},"translatedAt":{"type":"string","format":"date-time"},"reviewedBy":{"type":["string","null"]},"stale":{"type":"boolean","description":"The authored text changed on a served field since the translation was produced."}},"required":["kind","translatedAt","reviewedBy","stale"]},"ApiErrorEntry":{"type":"object","description":"Error taxonomy entry returned inside the envelope `errors[]` array.","properties":{"code":{"type":"string","enum":["ILLEGAL_TRANSITION","INTERNAL_ERROR","INVALID_PARAM","NOT_FOUND","RATE_LIMITED","SEMANTIC_ERROR","UNAUTHENTICATED","UNAUTHORIZED","UNAVAILABLE","VALIDATION_ERROR"],"example":"NOT_FOUND"},"message":{"type":"string","example":"Source not found"},"field":{"type":["string","null"],"description":"Field path that triggered the error (validation only)"}},"required":["code","message"]},"ApiErrorEnvelope":{"type":"object","description":"Envelope returned on any non-2xx Module #0 response.","properties":{"data":{"type":"null"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"minItems":1}},"required":["data","meta","errors"]},"PeoplesListEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PeopleV2"}},"meta":{"allOf":[{"$ref":"#/components/schemas/ApiResponseMeta"},{"required":["pagination"]}]},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"PatronymeLinkV2":{"type":"object","description":"A name as a fiche lists it — enough to name it and to link to its dossier.","properties":{"id":{"type":"string","pattern":"^PAT_[A-Z0-9_]+$","example":"PAT_KEITA"},"nameMain":{"type":"string","example":"Keïta"},"nameSystem":{"type":"string","enum":["clan_name","non_hereditary_patronymic","nisba","praise_name","totemic_clan"]}},"required":["id","nameMain","nameSystem"]},"PatronymeReachV2":{"allOf":[{"$ref":"#/components/schemas/PatronymeLinkV2"},{"type":"object","properties":{"viaPeoples":{"type":"array","description":"The peoples of this country that bear the name. They are what makes the entry reach rather than attestation, and what lets a reader audit the inference.","items":{"type":"object","properties":{"id":{"type":"string","example":"PPL_ZENATA"},"nameMain":{"type":"string","example":"Zénètes"}},"required":["id","nameMain"]}}},"required":["viaPeoples"]}]},"CountryPatronymesV2":{"type":"object","description":"The two name routes a country answers along, kept apart. They assert different things and neither contains the other — measured on the published fiches, 2 countries are reachable only directly and 6 only through their peoples. Summing them publishes an inference under the heading of a sourced fact.","properties":{"attested":{"type":"array","description":"Names a source attests in this country (afrik_patronyme_countries).","items":{"$ref":"#/components/schemas/PatronymeLinkV2"}},"borneByPeoples":{"type":"array","description":"Names borne by this country's peoples that are not attested here. The reader is told where the bearers live, not where the name is recorded.","items":{"$ref":"#/components/schemas/PatronymeReachV2"}}},"required":["attested","borneByPeoples"]},"PeopleDetailV2":{"allOf":[{"$ref":"#/components/schemas/PeopleV2"},{"type":"object","properties":{"patronymes":{"type":"array","description":"The names this people bears. Distinct from the ethnonym dossier at /peoples/{id}/names, which holds what the people is called. An empty array is the ordinary state of the fiches, not an omission.","items":{"$ref":"#/components/schemas/PatronymeLinkV2"}}},"required":["patronymes"]}]},"CountryDetailV2":{"allOf":[{"$ref":"#/components/schemas/CountryV2"},{"type":"object","properties":{"patronymes":{"$ref":"#/components/schemas/CountryPatronymesV2"}},"required":["patronymes"]}]},"PeopleDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PeopleDetailV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"CountriesListEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CountryV2"}},"meta":{"allOf":[{"$ref":"#/components/schemas/ApiResponseMeta"},{"required":["pagination"]}]},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"CountryDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CountryDetailV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"LanguageFamiliesListEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LanguageFamilyV2"}},"meta":{"allOf":[{"$ref":"#/components/schemas/ApiResponseMeta"},{"required":["pagination"]}]},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"DossierReadingV2":{"type":"object","description":"One reading of the chapter's subject. Comparative readings are optional; when present, both stances are published.","properties":{"stance":{"type":"string","enum":["official","counter"]},"label":{"type":"string"},"body":{"type":"string"},"sourceRefs":{"type":"array","items":{"type":"string"},"minItems":1,"description":"Keys into the dossier's own sources array."}},"required":["stance","label","body","sourceRefs"]},"DossierChapterV2":{"type":"object","properties":{"chapterKey":{"type":"string"},"ordinal":{"type":"integer"},"title":{"type":"string"},"question":{"type":"string"},"standfirst":{"type":"string"},"body":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string"},"sourceRefs":{"type":"array","items":{"type":"string"}}},"required":["text","sourceRefs"]}},"illustration":{"type":["object","null"],"description":"Credits for the chapter's document. Where the licence requires attribution, author and licenceUrl are both present.","properties":{"src":{"type":"string"},"alt":{"type":"string"},"caption":{"type":"string"},"author":{"type":["string","null"]},"licence":{"type":"string"},"licenceUrl":{"type":["string","null"]},"filePage":{"type":["string","null"]},"year":{"type":["string","null"]}},"required":["src","alt","caption","licence"]},"readings":{"type":"array","items":{"$ref":"#/components/schemas/DossierReadingV2"},"minItems":0},"figures":{"type":"array","items":{"type":"object","properties":{"figureKey":{"type":"string"},"label":{"type":"string"},"value":{"type":"string"},"year":{"type":"integer"},"note":{"type":["string","null"]},"sourceRefs":{"type":"array","items":{"type":"string"}}},"required":["figureKey","label","value","year","sourceRefs"]}}},"required":["chapterKey","ordinal","title","question","standfirst","body","illustration","readings","figures"]},"DossierV2":{"type":"object","properties":{"id":{"type":"string","example":"DOS_PROPORTIONS"},"vertical":{"type":"string","enum":["realites","nommer","histoires","spiritualites"]},"slug":{"type":"string","example":"proportions"},"publishedOn":{"type":"string","format":"date"},"title":{"type":"string"},"question":{"type":"string"},"standfirst":{"type":"string"},"thesis":{"type":"object","properties":{"stepLabel":{"type":"string"},"heading":{"type":"string"},"figures":{"type":"array","items":{"type":"object","properties":{"figureKey":{"type":"string"},"value":{"type":"string"},"claim":{"type":"string"},"provenance":{"type":"string"},"year":{"type":"integer"},"sourceRefs":{"type":"array","items":{"type":"string"}}},"required":["figureKey","value","claim","provenance","year","sourceRefs"]}}},"required":["stepLabel","heading","figures"]},"chapters":{"type":"array","items":{"$ref":"#/components/schemas/DossierChapterV2"}},"sources":{"type":"array","items":{"type":"object","properties":{"sourceKey":{"type":"string"},"title":{"type":"string"},"url":{"type":["string","null"]},"tier":{"type":"string","enum":["official","referenced","unverified"]},"source_kind":{"type":"string"},"publicationYear":{"type":"integer"},"notes":{"type":"string"}},"required":["sourceKey","title","url","tier"]}},"gaps":{"type":"array","items":{"type":"object","properties":{"fieldPath":{"type":"string"},"reason":{"type":"string"}},"required":["fieldPath","reason"]}}},"required":["id","vertical","slug","publishedOn","title","question","standfirst","thesis","chapters","sources","gaps"]},"DossierSummaryV2":{"type":"object","properties":{"id":{"type":"string"},"vertical":{"type":"string","enum":["realites","nommer","histoires","spiritualites"]},"slug":{"type":"string"},"publishedOn":{"type":"string","format":"date"},"title":{"type":"string"},"question":{"type":"string"},"standfirst":{"type":"string"},"chapterCount":{"type":"integer"},"sourceCount":{"type":"integer"}},"required":["id","vertical","slug","publishedOn","title","question","standfirst","chapterCount","sourceCount"]},"DossierDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DossierV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"DossierIndexEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DossierSummaryV2"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"LanguageFamilyDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LanguageFamilyV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"LanguageDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LanguageV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"PatronymeDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PatronymeV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"ApiKeyIssueEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"key":{"type":"string","description":"Raw API key, shown only once."},"tier":{"type":"string","const":"public"},"note":{"type":"string"}},"required":["key","tier","note"]},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"Source":{"type":"object","description":"Canonical bibliographic entry. Structured fields are nullable only during the lossless legacy-citation compatibility boundary.","properties":{"id":{"type":"string","format":"uuid"},"sourceKey":{"type":["string","null"],"description":"Stable source key. Null only for legacy entries awaiting review."},"sourceKind":{"type":["string","null"],"enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","community","repository","archive","discovery","ai_generated","unknown",null]},"tier":{"type":["string","null"],"enum":["official","referenced","unverified",null],"description":"Authority the source carries. Null means the citation has not been tiered yet."},"identifiers":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"Bibliographic or archival identifiers such as ISBN, DOI, catalogue, or call number."},"title":{"type":"string"},"url":{"type":["string","null"]},"pinnedUrl":{"type":["string","null"],"deprecated":true,"description":"Always null: no column backs this field. Kept because removing a published property is a breaking change, and null before reads the same as absent after."},"year":{"type":["integer","null"]},"author":{"type":["string","null"]},"publisher":{"type":["string","null"]},"resolvable":{"type":["boolean","null"],"deprecated":true,"description":"Always null: the nightly link check writes a log file, not a column. Kept for the same reason as pinnedUrl."},"lastVerifiedAt":{"type":["string","null"],"format":"date-time","description":"When a human last verified the source. Read from `verified_at`."},"notes":{"type":["string","null"],"description":"Why the source carries the tier it carries — the catalogue entry, domain rule, or citation form the tier was read from."},"page":{"type":["string","null"],"description":"Locator inside the work, when the citation named one."},"addedAt":{"type":["string","null"],"format":"date-time"},"policy":{"type":"object","properties":{"key":{"type":"string"},"tier":{"type":"string","enum":["official","referenced","unverified"]},"sourceKind":{"type":"string"}},"required":["key","tier","sourceKind"]}},"required":["id","title","policy"]},"SourceResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Source"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"SourceListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Source"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"ReferenceSource":{"type":"object","description":"Structured bibliographic reference returned by the contributor reference library.","properties":{"id":{"type":"string","format":"uuid"},"source_key":{"type":"string","maxLength":160},"title":{"type":"string","maxLength":1000},"author":{"type":"string"},"year":{"type":"integer","minimum":1000,"maximum":9999},"source_kind":{"type":"string","enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","community","repository","archive"]},"tier":{"type":"string","enum":["official","referenced","unverified"]},"identifiers":{"type":"object","additionalProperties":{"type":"string"}},"publisher":{"type":["string","null"]},"url":{"type":["string","null"],"format":"uri"}},"required":["id","source_key","title","author","year","source_kind","tier","identifiers","publisher","url"]},"ReferenceCreateInput":{"type":"object","properties":{"source_key":{"type":"string","minLength":1,"maxLength":160},"title":{"type":"string","minLength":1,"maxLength":1000},"authors":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","minLength":1,"maxLength":300}},"publication_year":{"type":"integer","minimum":1000,"maximum":9999},"source_kind":{"$ref":"#/components/schemas/ReferenceSourceKind"},"tier":{"type":"string","enum":["official","referenced","unverified"]},"identifiers":{"type":"object","additionalProperties":{"type":"string","maxLength":300},"default":{}},"publisher":{"type":["string","null"],"maxLength":500},"url":{"type":["string","null"],"format":"uri"}},"required":["source_key","title","authors","publication_year","source_kind","tier"]},"ReferenceSourceKind":{"type":"string","enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","community","repository","archive"]},"ReferenceCreateResult":{"type":"object","properties":{"source":{"$ref":"#/components/schemas/ReferenceSource"},"created":{"type":"boolean","description":"True when a new source was created; false when an existing matching source was returned."}},"required":["source","created"]},"AssertionReferenceCreateInput":{"type":"object","properties":{"assertion_id":{"type":"string","format":"uuid"},"source_id":{"type":"string","format":"uuid"},"locator_type":{"type":"string","enum":["page","folio","section","timestamp"]},"locator_value":{"type":"string","minLength":1,"maxLength":500}},"required":["assertion_id","source_id","locator_type","locator_value"]},"AssertionReference":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"assertion_id":{"type":"string","format":"uuid"},"source_id":{"type":"string","format":"uuid"},"locator_type":{"type":"string","enum":["page","folio","section","timestamp"]},"locator_value":{"type":"string"},"review_status":{"type":"string","enum":["verified","review_required"]}},"required":["id","assertion_id","source_id","locator_type","locator_value","review_status"]},"ReferenceWorkingAssetCreateInput":{"type":"object","description":"Multipart private working-asset upload. The submitted file is write-only and never returned by this API.","properties":{"sourceId":{"type":"string","format":"uuid"},"assetKind":{"type":"string","enum":["scan","ocr"]},"file":{"type":"string","format":"binary","writeOnly":true,"description":"Private scan or OCR asset to store."}},"required":["sourceId","assetKind","file"]},"PrivateReferenceWorkingAsset":{"type":"object","description":"Metadata for a private working asset. Responses intentionally omit binary content, bucket identifiers, and storage object or location paths; this endpoint does not grant asset retrieval access.","properties":{"id":{"type":"string","format":"uuid"},"sourceId":{"type":"string","format":"uuid"},"assetKind":{"type":"string","enum":["scan","ocr"]},"filename":{"type":"string"},"contentType":{"type":"string"},"byteSize":{"type":"integer","minimum":1,"maximum":26214400},"rightsStatus":{"type":"string","enum":["private"],"description":"The asset remains private to the working library."},"createdAt":{"type":"string","format":"date-time"}},"required":["id","sourceId","assetKind","filename","contentType","byteSize","rightsStatus","createdAt"]},"ReferenceSearchResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ReferenceSource"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"ReferenceCreateResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ReferenceCreateResult"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"AssertionReferenceCreateResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AssertionReference"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"PrivateReferenceWorkingAssetResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PrivateReferenceWorkingAsset"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"OralNarrative":{"type":"object","description":"A public, rights-cleared oral narrative, reviewed or not yet reviewed (reviewed narratives are listed first). This representation intentionally excludes transcripts, media locators, collector details, carrier references, reviewer identity, and withheld narrator names.","properties":{"id":{"type":"string","format":"uuid"},"narrativeCode":{"type":"string","example":"ORL_YORUBA_MEMORY_001"},"narratorDisplayName":{"type":["string","null"]},"community":{"type":"string"},"languageCode":{"type":"string","example":"yor"},"narrativeKind":{"type":"string","enum":["tradition","testimony","memory","story"]},"summary":{"type":["string","null"]},"variantOf":{"type":["string","null"],"format":"uuid"},"reviewed":{"type":"boolean","description":"Whether the narrative has been reviewed. An unreviewed narrative is shown labelled as not yet reviewed."}},"required":["id","narrativeCode","narratorDisplayName","community","languageCode","narrativeKind","summary","variantOf","reviewed"]},"OralNarrativeListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OralNarrative"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"Media":{"type":"object","description":"A media credit (author, licence URI, source page) attached to a fiche. Carries no binary media content or image URL — matching an actual image to a fiche is a separate, manually-curated step (ETNI-1412).","properties":{"id":{"type":"string","format":"uuid"},"entityType":{"type":"string","enum":["language_family","language","people","country"]},"entityId":{"type":"string","example":"PPL_SHONA"},"author":{"type":["string","null"]},"licenceUri":{"type":"string","format":"uri","example":"https://creativecommons.org/licenses/by-sa/4.0/"},"sourcePageUrl":{"type":["string","null"],"format":"uri"},"period":{"type":["string","null"]},"depictionTiming":{"type":"string","enum":["contemporary","reconstitution"]}},"required":["id","entityType","entityId","author","licenceUri","sourcePageUrl","period","depictionTiming"]},"MediaListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Media"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"ConfidenceRecord":{"type":"object","description":"Pre-computed confidence record for a Module #1 fiche. Populated by the `recompute_confidence(entity_type, entity_id)` Postgres function.","properties":{"entityType":{"type":"string","enum":["people","language-family"]},"entityId":{"type":"string","example":"PPL_SHONA"},"score":{"type":["number","null"],"minimum":0,"maximum":100,"example":73},"sourceCount":{"type":"integer","minimum":0},"avgSourceQuality":{"type":["number","null"],"minimum":0,"maximum":1},"lastHumanAuditAt":{"type":["string","null"],"format":"date-time"},"openFlagCount":{"type":"integer","minimum":0},"recomputedAt":{"type":["string","null"],"format":"date-time"}},"required":["entityType","entityId","sourceCount","openFlagCount"]},"ConfidenceResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ConfidenceRecord"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"ProvenanceCensus":{"type":"object","description":"How many of a fiche's assertions rest on each source standing, each assertion counted once under the standing of its strongest source. Deliberately not a score: an aggregate over chapters of unequal provenance describes none of them, so no field here averages anything. `assertionCount` is always the sum of the four standings; an assertion citing no source is excluded rather than given a bucket.","properties":{"entityType":{"type":"string","enum":["people","country","language","language-family"]},"entityId":{"type":"string","example":"CIV"},"assertionCount":{"type":"integer","minimum":0,"example":21},"standings":{"type":"object","description":"One count per value of the published source scale. `needs_review` is a transitional marker, not a fourth tier, and it also covers a source row the database left untiered.","properties":{"official":{"type":"integer","minimum":0,"example":4},"referenced":{"type":"integer","minimum":0,"example":11},"unverified":{"type":"integer","minimum":0,"example":4},"needs_review":{"type":"integer","minimum":0,"example":2}},"required":["official","referenced","unverified","needs_review"]},"lastHumanAuditAt":{"type":["string","null"],"format":"date-time"}},"required":["entityType","entityId","assertionCount","standings","lastHumanAuditAt"]},"ProvenanceCensusResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProvenanceCensus"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"ColonialOrigin":{"type":"object","description":"Present only once the colonial-borders dataset (Story 13.3) documents the pair (NFR31 — additive, optional).","properties":{"layerId":{"type":"string"},"sourceIds":{"type":"array","items":{"type":"string"}}},"required":["layerId","sourceIds"]},"FragmentationBorderPair":{"type":"object","properties":{"a":{"type":"string","example":"GHA"},"b":{"type":"string","example":"TGO"},"colonialOrigin":{"$ref":"#/components/schemas/ColonialOrigin"}},"required":["a","b"]},"FragmentationCountry":{"type":"object","properties":{"iso3":{"type":"string","example":"GHA"},"nameFr":{"type":"string","example":"Ghana"},"populationShare":{"type":"number","minimum":0,"maximum":1,"example":0.62},"assertionId":{"type":["string","null"],"description":"Backing assertion id, null when no per-field assertion exists yet for this fiche (bulk-migrated data predates the contribution workflow)."}},"required":["iso3","nameFr","populationShare","assertionId"]},"PeopleFragmentation":{"type":"object","description":"Derived strictly from afrik_people_countries + content.demography.distributionByCountry — no data foundation of its own (Epic 13).","properties":{"peopleId":{"type":"string","example":"PPL_EWE"},"autonym":{"type":["string","null"]},"exonym":{"type":["string","null"]},"countryCount":{"type":"integer","minimum":2},"countries":{"type":"array","items":{"$ref":"#/components/schemas/FragmentationCountry"}},"borderPairs":{"type":"array","items":{"$ref":"#/components/schemas/FragmentationBorderPair"}}},"required":["peopleId","autonym","exonym","countryCount","countries","borderPairs"]},"PeopleFragmentationResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PeopleFragmentation"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"RelationNeighbor":{"type":"object","properties":{"id":{"type":"string","example":"PPL_EWE"},"nameMain":{"type":"string"},"languageFamilyId":{"type":"string","example":"FLG_KWA"},"selfAppellation":{"type":"string","description":"The name the people gives itself (content.appellations.selfAppellation), verbatim. Omitted when the fiche records none.","example":"Eʋeawó (les gens Ewe), Eʋe"}},"required":["id","nameMain","languageFamilyId"]},"RelationPeriod":{"type":"object","properties":{"startYear":{"type":["integer","null"]},"endYear":{"type":["integer","null"]},"label":{"type":"string"}},"required":["startYear","endYear","label"]},"RelationConfidence":{"type":["object","null"],"properties":{"score":{"type":"number","minimum":0,"maximum":100},"sourceCount":{"type":["integer","null"],"minimum":0}},"required":["score","sourceCount"]},"SourcedRelationItem":{"type":"object","description":"Sourced (never derived) relation, from the perspective of the ego people (Story 11.7).","properties":{"relationId":{"type":"string","example":"REL_SONINKE_MANDE_TRADE"},"type":{"type":"string","enum":["migratory","commercial","religious"]},"direction":{"type":"string","enum":["a_to_b","b_to_a","bidirectional"]},"otherPeople":{"$ref":"#/components/schemas/RelationNeighbor"},"period":{"$ref":"#/components/schemas/RelationPeriod"},"description":{"type":"string"},"confidence":{"$ref":"#/components/schemas/RelationConfidence"}},"required":["relationId","type","direction","otherPeople","period","description","confidence"]},"DerivedRelationItem":{"type":"object","description":"Read-time-computed linguistic link (FR73) — structurally distinct from a sourced relation, never a flag on a shared shape.","properties":{"type":{"type":"string","enum":["linguistic"]},"derived":{"type":"boolean","enum":[true]},"basis":{"type":"string","enum":["sharedLanguageFamily"]},"languageFamilyId":{"type":"string","example":"FLG_KWA"},"otherPeople":{"$ref":"#/components/schemas/RelationNeighbor"}},"required":["type","derived","basis","languageFamilyId","otherPeople"]},"EgoNetwork":{"type":"object","properties":{"peopleId":{"type":"string","example":"PPL_EWE"},"sourced":{"type":"array","items":{"$ref":"#/components/schemas/SourcedRelationItem"}},"derived":{"type":"array","items":{"$ref":"#/components/schemas/DerivedRelationItem"}}},"required":["peopleId","sourced","derived"]},"EgoNetworkResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EgoNetwork"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"RelationSourceRef":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":["string","null"]},"tier":{"type":["string","null"],"enum":["official","referenced","unverified",null]},"sourceKind":{"type":["string","null"],"enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","press","encyclopedia","ngo","missionary_database","community","repository","archive","discovery","ai_generated","unknown","oral_tradition","ethniafrica_synthesis",null],"description":"What kind of work the source is (provenance, orthogonal to tier). Null or absent when none is recorded."}},"required":["id","title","url","tier"]},"RelationRecord":{"type":"object","description":"Non-ego-centered relation record — both sides exposed directly (peopleIdA/peopleIdB) rather than as a single neighbor (Story 11.7).","properties":{"id":{"type":"string","example":"REL_SONINKE_MANDE_TRADE"},"relationType":{"type":"string","enum":["migratory","commercial","religious"]},"peopleIdA":{"type":"string","example":"PPL_SONINKE"},"peopleIdB":{"type":"string","example":"PPL_MANDE"},"direction":{"type":"string","enum":["a_to_b","b_to_a","bidirectional"]},"period":{"$ref":"#/components/schemas/RelationPeriod"},"description":{"type":"string"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/RelationSourceRef"}},"confidence":{"$ref":"#/components/schemas/RelationConfidence"}},"required":["id","relationType","peopleIdA","peopleIdB","direction","period","description","sources","confidence"]},"RelationListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RelationRecord"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"RelationDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RelationRecord"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"GeoJSONGeometry":{"type":"object","description":"Schematic (corridor-level) GeoJSON geometry — never a claim of precise historical borders.","properties":{"type":{"type":"string","enum":["LineString","MultiLineString","Polygon"]},"coordinates":{"type":"array","description":"Nesting depth depends on `type` ([lon, lat] pairs for LineString; arrays thereof for MultiLineString/Polygon)."}},"required":["type","coordinates"]},"MigrationTimeRange":{"type":"object","properties":{"startYear":{"type":"integer","description":"Astronomical year integer; negative values are BCE.","example":-3000},"endYear":{"type":"integer","example":-1500},"datingNote":{"type":["string","null"]}},"required":["startYear","endYear","datingNote"]},"MigrationPeopleRef":{"type":"object","properties":{"id":{"type":"string","example":"PPL_KONGO"},"nameMain":{"type":"string"},"role":{"type":["string","null"],"example":"destination"}},"required":["id","nameMain","role"]},"MigrationSourceRef":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":["string","null"]},"tier":{"type":["string","null"],"enum":["official","referenced","unverified",null]},"sourceKind":{"type":["string","null"],"enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","press","encyclopedia","ngo","missionary_database","community","repository","archive","discovery","ai_generated","unknown","oral_tradition","ethniafrica_synthesis",null],"description":"What kind of work the source is (provenance, orthogonal to tier). Null or absent when none is recorded."}},"required":["id","title","url","tier"]},"MigrationEventSummary":{"type":"object","description":"List-view migration event — no geometry (AR18 payload-size discipline).","properties":{"id":{"type":"string","example":"MGR_BANTU_HOMELAND_DISPERSAL"},"nameMain":{"type":"string"},"migrationGroup":{"type":["string","null"],"example":"bantu-expansion"},"eventType":{"type":"string","enum":["expansion","trade_route","forced_displacement","pastoral_movement","fragmentation","displacement","imposed_name","resistance"]},"classificationStatus":{"type":"string","enum":["consensual","contested","colonial-legacy","reconstructive"]},"timeRange":{"$ref":"#/components/schemas/MigrationTimeRange"},"summary":{"type":"string"}},"required":["id","nameMain","migrationGroup","eventType","classificationStatus","timeRange","summary"]},"MigrationEventDetail":{"type":"object","description":"Detail-view migration event, incl. geometry, narrative, peoples and sources.","allOf":[{"$ref":"#/components/schemas/MigrationEventSummary"},{"type":"object","properties":{"geometry":{"$ref":"#/components/schemas/GeoJSONGeometry"},"narrative":{"type":"string"},"debate":{"type":["string","null"]},"peoples":{"type":"array","items":{"$ref":"#/components/schemas/MigrationPeopleRef"}},"sources":{"type":"array","items":{"$ref":"#/components/schemas/MigrationSourceRef"}}},"required":["geometry","narrative","debate","peoples","sources"]}]},"MigrationListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MigrationEventSummary"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"MigrationDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MigrationEventDetail"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"NameRecordSource":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":["string","null"]},"year":{"type":["integer","null"]},"tier":{"type":["string","null"],"enum":["official","referenced","unverified",null]}},"required":["id","title","url","year","tier"]},"NameRecordImposition":{"type":"object","description":"Imposed-name context. Present whenever any of its four fields carries data — including contemporaryUsage on non-imposed endonyms (the illustrative dossier shape), so this is not gated on imposedBy alone.","properties":{"imposedBy":{"type":["string","null"]},"impositionPeriod":{"type":["string","null"]},"whyProblematic":{"type":["string","null"]},"contemporaryUsage":{"type":["string","null"]}},"required":["imposedBy","impositionPeriod","whyProblematic","contemporaryUsage"]},"NameRecordConfidence":{"type":"object","properties":{"score":{"type":"number"},"recomputedAt":{"type":["string","null"]}},"required":["score","recomputedAt"]},"NameRecord":{"type":"object","description":"One name entry in a people's names dossier (endonym, exonym, historical spelling, or surname), with per-record sources and the people's confidence score (AR17 batched, not per-record queries).","properties":{"id":{"type":"string","example":"nr-endonym-1"},"nameText":{"type":"string","example":"Jieng"},"nameType":{"type":"string","enum":["endonym","exonym","historical_spelling","surname"]},"languageOfOrigin":{"type":["string","null"],"example":"din"},"meaning":{"type":["string","null"]},"periodLabel":{"type":["string","null"]},"imposition":{"oneOf":[{"$ref":"#/components/schemas/NameRecordImposition"},{"type":"null"}]},"assertionId":{"type":"string"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/NameRecordSource"}},"confidence":{"oneOf":[{"$ref":"#/components/schemas/NameRecordConfidence"},{"type":"null"}]},"attestations":{"type":"array","description":"The form's history (REQ-189): every time it was written down, by whom, with the source at a printed page. Empty when no attestation is recorded.","items":{"$ref":"#/components/schemas/NameAttestation"}},"shortLine":{"type":["string","null"],"description":"One sentence a reader understands without context (REQ-191), at most 120 characters."},"namedBy":{"type":["string","null"],"description":"Who gave the form: a people or an authority, never an author. Null for a self-name.","example":"Hausa"},"originDebated":{"type":"boolean"},"usedIn":{"type":"array","items":{"type":"string"},"example":["eng"]},"pronunciation":{"oneOf":[{"$ref":"#/components/schemas/NamePronunciation"},{"type":"null"}]}},"required":["id","nameText","nameType","languageOfOrigin","meaning","periodLabel","imposition","assertionId","sources","confidence","attestations","shortLine","namedBy","originDebated","usedIn","pronunciation"]},"NamePronunciation":{"type":"object","description":"How the self-name is said (REQ-191): a respelling in the site's language, an optional recording with the speaker's consent, and its source.","properties":{"respelling":{"type":"string","example":"fool-beh"},"audio":{"oneOf":[{"type":"object","properties":{"url":{"type":"string"},"consent":{"type":"string"}},"required":["url","consent"]},{"type":"null"}]},"source":{"type":"object","properties":{"title":{"type":"string"},"author":{"type":"string"},"year":{"type":"integer"},"url":{"type":"string"},"tier":{"type":"string"},"page":{"type":"string"}},"required":["title","author","year","url","tier"]}},"required":["respelling","audio","source"]},"NameAttestation":{"type":"object","properties":{"formAsWritten":{"type":"string","example":"Fulbe"},"year":{"type":["integer","null"],"example":1352},"periodLabel":{"type":["string","null"],"example":"14th century"},"attestedBy":{"type":"string","example":"Ibn Battuta"},"source":{"type":"object","properties":{"title":{"type":"string"},"author":{"type":"string"},"year":{"type":"integer"},"url":{"type":"string"},"tier":{"type":"string"},"page":{"type":"string","example":"p. 192"}},"required":["title","author","year","url","tier","page"]}},"required":["formAsWritten","year","periodLabel","attestedBy","source"]},"PeopleNamesDossier":{"type":"object","properties":{"peopleId":{"type":"string","example":"PPL_DINKA"},"autonym":{"type":["string","null"]},"names":{"type":"array","items":{"$ref":"#/components/schemas/NameRecord"}}},"required":["peopleId","autonym","names"]},"PeopleNamesDossierResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PeopleNamesDossier"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"FamilyTreeBranchNode":{"type":"object","properties":{"id":{"type":"string","example":"PPL_BAKONGO"},"nameMain":{"type":"string","example":"Bakongo"},"classificationStatus":{"type":["string","null"],"enum":["consensual","contested","colonial-legacy","reconstructive",null]}},"required":["id","nameMain","classificationStatus"]},"LanguageFamilyTreeBranchResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FamilyTreeBranchNode"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"FamilyTreeBranch":{"type":"object","properties":{"iso639_3":{"type":"string","example":"swa"},"name":{"type":"string","example":"Swahili"},"peopleCount":{"type":"integer","minimum":0,"example":3}},"required":["iso639_3","name","peopleCount"]},"FamilyTreeSkeletonFamily":{"type":"object","description":"Lightweight family header for the tree skeleton. Deliberately omits the full editorial `content` JSONB (see LanguageFamilyV2) to keep the skeleton payload small (ETNI-463 AC3: ≤ 15 KB for the largest family).","properties":{"id":{"type":"string","description":"Identifiant FLG_*","example":"FLG_BANTU"},"nameFr":{"type":"string","example":"Bantou"},"nameEn":{"type":"string","example":"Bantu"},"classificationStatus":{"type":["string","null"],"enum":["consensual","contested","colonial-legacy","reconstructive",null]}},"required":["id","nameFr"]},"LanguageFamilyTree":{"type":"object","description":"Classification tree skeleton: the family's languages (branches) with linked-people counts, plus peoples in the family not linked to any of its languages.","properties":{"family":{"$ref":"#/components/schemas/FamilyTreeSkeletonFamily"},"branches":{"type":"array","items":{"$ref":"#/components/schemas/FamilyTreeBranch"}},"branchProvenance":{"type":"string","enum":["language-corpus","people-fiches"],"description":"Which source produced `branches`: the language fiches, or a reconstruction from the ISO codes the people fiches declare. `afrik_languages` now holds 748 rows in recette, but coverage still varies by family, so `people-fiches` remains a common fallback."},"declaredBranches":{"type":"array","description":"Branch names the family fiche states in `generalInfo.branches`. No field ties a people to one of them, so they are a declared register rather than parents in the tree.","items":{"type":"string"},"example":["Atlantique central","Peul-Sérère"]},"unlinkedPeopleCount":{"type":"integer","minimum":0,"example":1}},"required":["family","branches","branchProvenance","declaredBranches","unlinkedPeopleCount"]},"LanguageFamilyTreeResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LanguageFamilyTree"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"PeopleRevisionItem":{"type":"object","description":"A single published revision in the people revision history list.","properties":{"version":{"type":"integer","minimum":1,"example":3,"description":"Monotonically increasing publication version"},"published_at":{"type":["string","null"],"format":"date-time","example":"2026-05-21T10:00:00.000Z"},"moderator_pseudonym":{"type":["string","null"],"example":"mod-aaaabbbb","description":"Privacy-preserving pseudonym derived from the moderator's internal id"},"reason":{"type":["string","null"],"example":"Demographics update"},"pinned_url":{"type":"string","example":"/api/v2/peoples/PPL_YORUBA/versions/3","description":"Stable URL for this pinned version (AR14). Cache-Control: s-maxage=31536000, immutable."}},"required":["version","pinned_url"]},"CursorPaginationMeta":{"type":"object","description":"Cursor-based pagination meta (no offset).","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"example":20},"next_cursor":{"type":["integer","null"],"example":4,"description":"Version to pass as ?cursor= on the next request. Null when no more pages."}},"required":["limit","next_cursor"]},"PeopleRevisionListMeta":{"type":"object","properties":{"license":{"type":"string","example":"CC-BY-SA-4.0"},"attribution":{"type":"string","example":"EthniAfrica — ethniafrica.com"},"pagination":{"$ref":"#/components/schemas/CursorPaginationMeta"}},"required":["license","attribution","pagination"]},"PeopleRevisionListResponse":{"type":"object","description":"Cursor-paginated list of published revisions ordered by version DESC.","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PeopleRevisionItem"}},"meta":{"$ref":"#/components/schemas/PeopleRevisionListMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"PeopleVersionSnapshotResponse":{"type":"object","description":"Full published snapshot at a pinned version. Data is read from the immutable revision record, never from the live entity (AR14). Response is permanently cacheable (AR18).","properties":{"data":{"type":"object","description":"Full denormalised entity state as stored at publication time (snapshot_jsonb)"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"DoctrineEntry":{"type":"object","description":"Editorial-doctrine row (MDX source stored in DB).","properties":{"slug":{"type":"string","enum":["review_policy","naming_convention","ai_disclosure","license_attribution"]},"title":{"type":"string"},"mdxSource":{"type":"string"},"version":{"type":"integer","minimum":1},"publishedAt":{"type":["string","null"],"format":"date-time"}},"required":["slug","title","mdxSource","version"]},"DoctrineResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DoctrineEntry"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}}},"FeedRevisionItem":{"type":"object","description":"A single published revision entry in the cross-entity revisions feed.","properties":{"entity_type":{"type":"string","enum":["people","country","languageFamily"],"example":"people"},"entity_id":{"type":"string","example":"PPL_YORUBA","description":"Stable entity identifier (PPL_*, FLG_*, ISO 3166-1 alpha-3)"},"slug":{"type":"string","example":"ppl_yoruba","description":"URL-friendly lowercase form of entity_id"},"version":{"type":"integer","minimum":1,"example":3,"description":"Monotonically increasing publication version"},"published_at":{"type":["string","null"],"format":"date-time","example":"2026-05-21T12:00:00.000Z"},"pinned_url":{"type":"string","example":"/api/v2/peoples/PPL_YORUBA/versions/3","description":"Stable pinned-version URL (AR14)"},"summary":{"type":["string","null"],"example":"Demographics update","description":"Editorial reason for the revision, if provided"}},"required":["entity_type","entity_id","slug","version","pinned_url"]},"FeedCursorPaginationMeta":{"type":"object","description":"Cursor-based pagination meta for the revisions feed.","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"example":20},"next_cursor":{"type":["string","null"],"example":"MjAyNi0wNS0yMVQxMjowMDowMC4wMDB...","description":"Opaque base64url cursor. Pass as ?cursor= on the next request. Null when no more pages."}},"required":["limit","next_cursor"]},"FeedRevisionListMeta":{"type":"object","properties":{"license":{"type":"string","example":"CC-BY-SA-4.0"},"attribution":{"type":"string","example":"EthniAfrica — ethniafrica.com"},"pagination":{"$ref":"#/components/schemas/FeedCursorPaginationMeta"}},"required":["license","attribution","pagination"]},"FeedRevisionListResponse":{"type":"object","description":"Cursor-paginated list of published revisions across all entity types (JSON format).","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FeedRevisionItem"}},"meta":{"$ref":"#/components/schemas/FeedRevisionListMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"FlagKind":{"type":"string","enum":["inaccurate","missing-source","broken-url","offensive","correction-proposal","other","contribution"],"example":"inaccurate"},"FlagStatus":{"type":"string","enum":["open","under_review","accepted","rejected","withdrawn","duplicate"],"example":"open"},"FlagCreateInput":{"type":"object","required":["target_type","target_id","flag_kind","reason_text","antibot"],"properties":{"target_type":{"type":"string","example":"people","description":"AFRIK entity type (people, country, language, language_family)"},"target_id":{"type":"string","example":"PPL_YORUBA","description":"Stable AFRIK identifier of the entity being flagged"},"target_field_path":{"type":"string","example":"demographics.population","description":"Optional dotted path to the disputed field"},"flag_kind":{"$ref":"#/components/schemas/FlagKind"},"reason_text":{"type":"string","minLength":10,"maxLength":2000,"example":"Population figure appears outdated vs. 2024 census."},"reporter_email":{"type":"string","format":"email","maxLength":320,"example":"lectrice@example.org","description":"Optional reply address. The report is created and published whether or not it is supplied. A single-use link is e-mailed to confirm the address, and only a confirmed address ever receives the moderation decision. Never published, and never returned by any endpoint."},"language":{"type":"string","enum":["fr"],"default":"fr","description":"Locale used for reporter verification and moderation-decision e-mails. French is the only one published; missing or other values fail closed to French."},"counter_source_url":{"type":"string","format":"uri","example":"https://example.org/census/2024"},"counter_source_citation":{"type":"string","maxLength":2000,"example":"National Statistics Office, 2024 census, table 12."},"proposed_rewrite":{"type":"string","maxLength":5000,"example":"Update the population figure using the 2024 census."},"antibot":{"type":"object","writeOnly":true,"required":["salt","nonce","difficultyBits","expiresAt","signature"],"description":"A solved proof-of-work challenge, obtained from GET /v2/antibot/challenge. Single-use and short-lived. Nothing in it identifies the caller.","properties":{"salt":{"type":"string","example":"9f2c1ab4d7e60358"},"nonce":{"type":"string","example":"418209"},"difficultyBits":{"type":"integer","example":20},"expiresAt":{"type":"integer","example":1788080000000},"signature":{"type":"string","example":"3b1f…"}}},"elapsedMs":{"type":"integer","writeOnly":true,"example":18400,"description":"Milliseconds the form was open before submission. A submission faster than a person could have written it is refused."}},"example":{"target_type":"people","target_id":"PPL_YORUBA","target_field_path":"demographics.population","flag_kind":"inaccurate","reason_text":"Population figure appears outdated compared with the latest census.","counter_source_url":"https://example.org/census/2024","counter_source_citation":"National Statistics Office, 2024 census, table 12.","proposed_rewrite":"Update the population figure using the 2024 census.","language":"fr","antibot":{"salt":"9f2c1ab4d7e60358","nonce":"418209","difficultyBits":20,"expiresAt":1788080000000,"signature":"3b1f…"},"elapsedMs":18400}},"FlagCreated":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd"},"public_slug":{"type":"string","example":"flag-7kq3m2"},"status":{"$ref":"#/components/schemas/FlagStatus"},"created_at":{"type":"string","format":"date-time","example":"2026-07-24T10:15:30.000Z"}},"required":["id","public_slug","status","created_at"],"example":{"id":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd","public_slug":"flag-7kq3m2","status":"open","created_at":"2026-07-24T10:15:30.000Z"}},"PublicFlag":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd"},"public_slug":{"type":"string","example":"flag-7kq3m2"},"target_type":{"type":["string","null"],"example":"people"},"target_id":{"type":["string","null"],"example":"PPL_YORUBA"},"target_field_path":{"type":["string","null"],"example":"demographics.population"},"assertion_id":{"type":["string","null"],"format":"uuid","example":null},"flag_kind":{"$ref":"#/components/schemas/FlagKind"},"reason_text":{"type":["string","null"],"example":"Population figure appears outdated compared with the latest census."},"counter_source_url":{"type":["string","null"],"format":"uri","example":"https://example.org/census/2024"},"counter_source_citation":{"type":["string","null"],"example":"National Statistics Office, 2024 census, table 12."},"proposed_rewrite":{"type":["string","null"],"example":"Update the population figure using the 2024 census."},"contributor_id":{"type":["string","null"],"format":"uuid","example":"bdbb6b42-3890-4c80-ac96-f732908d17c7"},"severity":{"type":["string","null"],"enum":["low","medium","high","critical",null],"example":null},"auto_generated":{"type":"boolean","example":false},"status":{"$ref":"#/components/schemas/FlagStatus"},"created_at":{"type":"string","format":"date-time","example":"2026-07-24T10:15:30.000Z"},"updated_at":{"type":["string","null"],"format":"date-time","example":"2026-07-24T11:00:00.000Z"},"resolved_at":{"type":["string","null"],"format":"date-time","example":null},"moderator_notes":{"type":["string","null"],"description":"The moderator's answer on a decided report. Required by the moderation charter on accepted, rejected and duplicate.","example":null},"remediation_state":{"type":["string","null"],"enum":["not_started","in_progress","published","not_applicable",null],"description":"What changed in the published fiches, separately from what the moderators thought of the report. Written by the publication of a correction, never by a moderator decision. NULL while the report is open or under review.","example":null},"remediation_published_at":{"type":["string","null"],"format":"date-time","description":"When the correction reached the published fiches. Never null when remediation_state is published.","example":null},"remediation_summary":{"type":["string","null"],"description":"One reader-facing sentence naming what changed, shown verbatim on the public report page.","example":null}},"required":["id","public_slug","target_type","target_id","target_field_path","assertion_id","flag_kind","reason_text","counter_source_url","counter_source_citation","proposed_rewrite","contributor_id","severity","auto_generated","status","created_at","updated_at","resolved_at","moderator_notes","remediation_state","remediation_published_at","remediation_summary"],"example":{"id":"9c81ca0d-ae45-4f08-8f53-2ac0a9673abd","public_slug":"flag-7kq3m2","target_type":"people","target_id":"PPL_YORUBA","target_field_path":"demographics.population","assertion_id":null,"flag_kind":"inaccurate","reason_text":"Population figure appears outdated compared with the latest census.","counter_source_url":"https://example.org/census/2024","counter_source_citation":"National Statistics Office, 2024 census, table 12.","proposed_rewrite":"Update the population figure using the 2024 census.","contributor_id":"bdbb6b42-3890-4c80-ac96-f732908d17c7","severity":null,"auto_generated":false,"status":"open","created_at":"2026-07-24T10:15:30.000Z","updated_at":"2026-07-24T11:00:00.000Z","resolved_at":null,"moderator_notes":null,"remediation_state":null,"remediation_published_at":null,"remediation_summary":null}},"FlagCursorPaginationMeta":{"type":"object","description":"Opaque cursor pagination for the public flags list.","properties":{"limit":{"type":"integer","minimum":1,"maximum":100,"default":20,"example":20},"next_cursor":{"type":["string","null"],"example":"MjAyNi0wNy0yNFQxMDoxNTozMC4wMDBafDljODFjYTBk","description":"Opaque cursor to pass unchanged as ?cursor=. Null when no more pages remain."}},"required":["limit","next_cursor"]},"FlagListMeta":{"type":"object","properties":{"license":{"type":"string","example":"CC-BY-SA-4.0"},"attribution":{"type":"string","example":"EthniAfrica — ethniafrica.com"},"pagination":{"$ref":"#/components/schemas/FlagCursorPaginationMeta"}},"required":["license","attribution","pagination"]},"FlagCreatedResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/FlagCreated"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"FlagListResponse":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicFlag"}},"meta":{"$ref":"#/components/schemas/FlagListMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"FlagDetailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicFlag"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"FlagAuditEntry":{"type":"object","description":"One recorded step in a report's life. It names the role that acted and the level it was authorised at, never the person: a register has to establish that a decision was taken and at what level of authorisation, and a public register backed by an internal console has no reason to go further.","properties":{"event":{"type":"string","enum":["received","under_review","accepted","rejected","duplicate","withdrawn","revision_linked"],"example":"accepted"},"occurredAt":{"type":"string","format":"date-time","description":"When the step was recorded. The console renders it in UTC, so a moderation timeline reads the same from every time zone.","example":"2026-09-16T08:02:00.000Z"},"actorRole":{"type":"string","enum":["reader","moderator"],"example":"moderator"},"authorisationLevel":{"type":["string","null"],"enum":["admin",null],"description":"The level the actor held, or null for a reader. The moderation allowlist is flat, so `admin` is the only level the console grants.","example":"admin"}},"required":["event","occurredAt","actorRole","authorisationLevel"]},"FlagAuditTrail":{"type":"object","properties":{"publicSlug":{"type":"string","example":"00EZK83QDV"},"entries":{"type":"array","description":"Oldest first. The first entry is the report's arrival, credited to a reader.","items":{"$ref":"#/components/schemas/FlagAuditEntry"}}},"required":["publicSlug","entries"]},"FlagAuditTrailResponse":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/FlagAuditTrail"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"QuizScopeOption":{"type":"object","description":"One track a session can be drawn from. `playable` is false when the track cannot fill a session of eight — it is still listed, and still counted, rather than hidden.","properties":{"id":{"type":"string","example":"GHA"},"labelFr":{"type":"string","example":"Ghana"},"activeQuestionCount":{"type":"integer","minimum":0},"playable":{"type":"boolean"},"playableThemeIds":{"type":"array","items":{"type":"string"},"description":"The themes this track can fill a session of, in the picker's order. A theme absent from the list cannot be crossed with this track — `?pays=GHA&theme=…` naming an omitted theme answers 422.","example":["noms","langues","croyances"]}},"required":["id","labelFr","activeQuestionCount","playable","playableThemeIds"]},"QuizThemeOption":{"type":"object","description":"One domain of content a session can be narrowed to, counted across all the published fiches.","properties":{"id":{"type":"string","enum":["noms","langues","parente-linguistique","territoire","rites-et-culture","croyances","royaumes-et-histoire","organisation","migrations"]},"labelFr":{"type":"string","example":"Croyances"},"activeQuestionCount":{"type":"integer","minimum":0},"playable":{"type":"boolean"}},"required":["id","labelFr","activeQuestionCount","playable"]},"QuizScopesData":{"type":"object","description":"GET /v2/quiz/scopes result data.","properties":{"countries":{"type":"array","items":{"$ref":"#/components/schemas/QuizScopeOption"}},"families":{"type":"array","items":{"$ref":"#/components/schemas/QuizScopeOption"}},"themes":{"type":"array","items":{"$ref":"#/components/schemas/QuizThemeOption"}},"mixed":{"$ref":"#/components/schemas/QuizScopeOption"},"random":{"$ref":"#/components/schemas/QuizScopeOption"}},"required":["countries","families","mixed","random"]},"QuizScopesResponse":{"type":"object","description":"Module #0 envelope for /v2/quiz/scopes (ETNI-496)","properties":{"data":{"$ref":"#/components/schemas/QuizScopesData"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"QuizScope":{"type":"object","description":"The track the returned session was drawn from.","properties":{"kind":{"type":"string","enum":["country","family","mixed","random"]},"entityId":{"type":["string","null"],"example":"GHA"},"labelFr":{"type":"string","example":"Ghana"}},"required":["kind","entityId","labelFr"]},"QuizSourceRef":{"type":"object","description":"The single highest-tier resolvable source backing the question's assertion (official or referenced, FR65 gate).","properties":{"title":{"type":"string"},"year":{"type":["integer","null"]},"tier":{"type":["string","null"],"enum":["official","referenced","unverified",null]},"url":{"type":["string","null"]},"sourceKind":{"type":["string","null"],"enum":["intergovernmental","government","official_statistics","linguistic_reference","academic","press","encyclopedia","ngo","missionary_database","community","repository","archive","discovery","ai_generated","unknown","oral_tradition","ethniafrica_synthesis",null],"description":"What kind of work the source is. Absent or null when the source records none."}},"required":["title","year","tier","url"]},"QuizEntityLink":{"type":"object","description":"The AFRIK entity the question's field value is drawn from.","properties":{"type":{"type":"string","enum":["people","country"]},"id":{"type":"string","example":"PPL_SHONA"},"slug":{"type":"string","example":"PPL_SHONA"},"autonym":{"type":["string","null"]},"exonym":{"type":["string","null"]}},"required":["type","id","slug","autonym","exonym"]},"QuizOptionValue":{"description":"A plain string for non-name fields, or a structured autonym/exonym name.","oneOf":[{"type":"string"},{"type":"object","properties":{"autonym":{"type":"string"},"exonym":{"type":"string"}},"required":["autonym"]}]},"QuizSessionQuestion":{"type":"object","description":"One session question. The answer key ships in the payload (correctOption, explanationFr, source) — reveal is client-side.","properties":{"id":{"type":"string","format":"uuid"},"templateId":{"type":"string","description":"T1-T4 ask about an atomic fiche field; T6-T11 quote a prose rubric of a people and ask which people it belongs to; T12 asks which of a people's exonyms is contested; T13-T18 do the same over countries. Kept in step with QUIZ_TEMPLATE_IDS by openapiV2 contract tests — the enum had gone stale twice.","enum":["T1","T2","T3","T4","T6","T7","T8","T9","T10","T11","T12","T13","T14","T15","T16","T17","T18"]},"promptFr":{"type":"string"},"stimulusFr":{"type":["string","null"],"description":"Verbatim fiche prose shown above the stem, on the templates whose answer is the subject. Null elsewhere."},"optionsFr":{"type":"array","minItems":4,"maxItems":4,"items":{"$ref":"#/components/schemas/QuizOptionValue"}},"correctOption":{"type":"integer","minimum":0,"maximum":3},"explanationFr":{"type":"string"},"source":{"$ref":"#/components/schemas/QuizSourceRef"},"assertionId":{"type":"string","format":"uuid"},"entity":{"$ref":"#/components/schemas/QuizEntityLink"}},"required":["id","templateId","promptFr","stimulusFr","optionsFr","correctOption","explanationFr","source","assertionId","entity"]},"QuizSessionData":{"type":"object","description":"GET /v2/quiz/session result data. Questions arrive in the order they are meant to be played: ascending difficulty band, except under `mode=aleatoire`.","properties":{"scope":{"$ref":"#/components/schemas/QuizScope"},"questions":{"type":"array","items":{"$ref":"#/components/schemas/QuizSessionQuestion"}}},"required":["scope","questions"]},"QuizSessionResponse":{"type":"object","description":"Module #0 envelope for /v2/quiz/session (ETNI-496)","properties":{"data":{"$ref":"#/components/schemas/QuizSessionData"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"}}},"required":["data","meta","errors"]},"ApiKeySummary":{"type":"object","description":"One of the caller's own API keys. Never carries the raw key or its hash.","properties":{"id":{"type":"string","format":"uuid"},"label":{"type":["string","null"]},"tier":{"type":"string","enum":["public","partner","admin"]},"active":{"type":"boolean"},"key_prefix":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"}},"required":["id","label","tier","active","key_prefix","created_at","last_used_at","expires_at","revoked_at"]},"ApiKeyCreated":{"type":"object","description":"Response to a successful key creation. `key` is the raw key — shown only in this response, never retrievable again.","allOf":[{"$ref":"#/components/schemas/ApiKeySummary"},{"type":"object","properties":{"key":{"type":"string"}},"required":["key"]}]},"PlaceSummaryV2":{"type":"object","description":"One row of the place list (REQ-196).","properties":{"id":{"type":"string","example":"LOC_YAMOUSSOUKRO"},"placeType":{"type":"string","enum":["ville","region","site-historique","autre"]},"nameMain":{"type":"string","example":"Yamoussoukro"},"countryId":{"type":["string","null"],"example":"CIV","description":"The place's country when it lies in one; null for a region that spans several (read countryIds)."},"countryIds":{"type":"array","items":{"type":"string"},"example":["CIV"],"description":"Every country the place lies in, in the fiche's order: one for a town, several for a region such as the Sahara."}},"required":["id","placeType","nameMain","countryId","countryIds"]},"PlaceV2":{"type":"object","description":"A place fiche (REQ-196). `names` lists every name the place answers to — its filed name, then each nameText of its nameHistory — with no form promoted over another; `nameHistory` tells where each comes from.","properties":{"id":{"type":"string","example":"LOC_YAMOUSSOUKRO"},"placeType":{"type":"string","enum":["ville","region","site-historique","autre"]},"nameMain":{"type":"string","example":"Yamoussoukro"},"names":{"type":"array","items":{"type":"string"},"example":["Yamoussoukro","N'Gokro"]},"summary":{"type":"string"},"country":{"type":["object","null"],"description":"The place's country when it lies in one; null for a region that spans several (read countries).","properties":{"id":{"type":"string","example":"CIV"},"name":{"type":["string","null"]}},"required":["id","name"]},"countries":{"type":"array","description":"Every country the place lies in, in the fiche's order: one for a town, several for a region such as the Sahara.","items":{"type":"object","properties":{"id":{"type":"string","example":"CIV"},"name":{"type":["string","null"]}},"required":["id","name"]}},"associatedPeoples":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"relation":{"type":["string","null"]}},"required":["id","name","relation"]}},"gaps":{"type":"array","items":{"type":"object"}},"sources":{"type":"array","items":{"type":"object"}},"nameHistory":{"oneOf":[{"$ref":"#/components/schemas/NameHistoryV2"},{"type":"null"}]}},"required":["id","placeType","nameMain","names","summary","country","countries","associatedPeoples","gaps","sources","nameHistory"]},"PlaceSearchHitV2":{"type":"object","description":"A place search hit (REQ-196): exact name match, then a prefix match on any of its names, then a pg_trgm fallback; normalizedScore is the cross-kind scale of migration 069.","properties":{"id":{"type":"string","example":"LOC_YAMOUSSOUKRO"},"nameMain":{"type":"string","example":"Yamoussoukro"},"placeType":{"type":"string","example":"ville"},"countryId":{"type":["string","null"],"example":"CIV","description":"The place's country when it lies in one; null for a region that spans several (read countryIds)."},"countryIds":{"type":"array","items":{"type":"string"},"example":["CIV"]},"summary":{"type":"string"},"nameHistory":{"oneOf":[{"$ref":"#/components/schemas/NameHistoryV2"},{"type":"null"}]},"relevance":{"type":"number"},"exactMatch":{"type":"boolean"},"normalizedScore":{"type":"number","minimum":0,"maximum":1}},"required":["id","nameMain","placeType","countryId","exactMatch","normalizedScore"]},"PlaceDetailEnvelope":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PlaceV2"},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]},"PlaceListEnvelope":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PlaceSummaryV2"}},"meta":{"$ref":"#/components/schemas/ApiResponseMeta"},"errors":{"type":"array","items":{"$ref":"#/components/schemas/ApiErrorEntry"},"maxItems":0}},"required":["data","meta","errors"]}}},"security":[{"BearerAuth":[]},{}]}