{"openapi":"3.1.0","tags":[{"name":"entries","description":"Create, read, update, and delete entries, their senses, and example sentences."}],"info":{"title":"Living Dictionaries Write API","version":"1.0.0","description":"Endpoint reference for the Living Dictionaries Write API — programmatic read/write access to ONE dictionary. An agent can do anything a human editor can.\n\n## Read your task guide before this document\nThe starting point is `GET /api/v1` — a small front door that routes you to the GUIDE for the job you are actually doing (importing, cleanup, bulk reads, media, corpus). If you came straight here, go there first. The guides carry the judgement calls (what to ask your human, what never to guess, how not to corrupt a language's data); this document carries only field shapes. `GET /api/v1/guides/api-basics` covers the mechanics every job shares: auth, the dictionary id, multilingual fields, generating your own ids for idempotency, batch results, errors, and size limits.\n\n## Auth\nEvery request carries `Authorization: Bearer ldk_…` — a key minted on the dictionary's Agents page, scoped to ONE dictionary, granting either read or read & write. A key for dictionary A cannot touch dictionary B (403). Every path's `{id}` is that dictionary's id or url-slug.\n\n## Fetching this document\nBy default `GET /api/v1/openapi.json` returns a COMPACT INDEX: every path with its method summaries and tag, plus the list of schema names. Read that, then fetch just the group you need WITH full ($ref-complete) schemas via `?tag=<name>` (tag names are in the top-level `tags` list). `?view=full` returns the complete ~200KB document — rarely what you want."},"servers":[{"url":"https://livingdictionaries.app"}],"security":[{"bearerAuth":[]}],"paths":{"/api/v1/dictionaries/{id}/entries":{"get":{"summary":"List/filter entries","description":"Entry summaries for verification / live counts. Ordered by `updated_at` ASC. Paginated via `limit` (default 100, max 500) + `offset`; `has_more` tells you when to fetch the next page. GOTCHA: `limit` is silently capped at 500 — advance `offset` by the number of entries RETURNED while `has_more` is true, never by your requested limit (a `returned < limit` break-condition silently truncates when limit > 500). (For idempotent imports, prefer a local `external_id`→`entry_id` ledger over paginating for dedupe.)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"elicitation_id","in":"query","schema":{"type":"string"},"description":"Exact match."},{"name":"lexeme","in":"query","schema":{"type":"string"},"description":"Match the headword. Substring (case-insensitive) by default; pass `match=exact` for an exact, case-sensitive match against any orthography's spelling."},{"name":"match","in":"query","schema":{"type":"string","enum":["substring","exact"],"default":"substring"},"description":"How `lexeme` is matched. `exact` matches a whole spelling in any locale (use it to check whether a specific headword already exists)."},{"name":"updated_since","in":"query","schema":{"type":"string","format":"date-time"},"description":"ISO timestamp, exclusive."},{"name":"include","in":"query","schema":{"type":"string","enum":["senses"]},"description":"Pass `senses` to attach each entry's senses (glosses/definition/POS/domains) in one batched query — the efficient way to bulk-read/export a dictionary's meanings without a per-entry request."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}}],"responses":{"200":{"description":"Entry summaries + pagination","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntriesListResponse"}}}}},"tags":["entries"]},"post":{"summary":"Create entries (bulk)","description":"Body: a single entry, a bare array, or `{ entries: EntryInput[], import_id? }`. Max 1000/request. Per-item best-effort — read `results` for per-entry outcomes.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/EntryInput"},{"type":"array","items":{"$ref":"#/components/schemas/EntryInput"}},{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/EntryInput"}},"import_id":{"type":"string"}},"required":["entries"]}]},"example":{"import_id":"swahili-pdf-2026","entries":[{"id":"3f1b8c9a-0d2e-4a6b-9c1f-2e5d7a8b4c10","lexeme":"mbwa","phonetic":"ˈᵐbwa","dialects":["Coastal"],"senses":[{"glosses":{"en":"dog","sw":"mbwa"},"parts_of_speech":["n"],"example_sentences":[{"text":"Mbwa wangu ni mkubwa","translation":{"en":"My dog is big"}}]}]}]}}}},"responses":{"200":{"description":"Per-item write report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntriesWriteResponse"}}}},"400":{"description":"Bad input"},"401":{"description":"Missing/invalid key"},"403":{"description":"Insufficient role or wrong dictionary"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/{entryId}":{"get":{"summary":"Read one entry (full nested)","description":"Returns the full nested entry (the READ shape — see `EntryResponse`). NOTE the input→output asymmetry: scalars you POST at the top level are read back under `entry.main`; `senses[].example_sentences` are read back as `senses[].sentences`. Pass `?include=relationships` to also attach `entry.relationships` (see `RelationshipView`).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}},{"name":"include","in":"query","schema":{"type":"string","enum":["relationships"]},"description":"Pass `relationships` to attach this entry's typed relationships as `entry.relationships`."}],"responses":{"200":{"description":"The full nested entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryResponse"}}}},"404":{"description":"Not found"}},"tags":["entries"]},"patch":{"summary":"Update an entry (field-merge)","description":"Field-merge: provided fields overwrite, omitted ones stay. `senses` are a true upsert by client `id` (id on this entry → field-merge; unknown id or none → create the sense WITH that id; an id on a different entry → 400); example sentences upsert by id (no duplicate links) / append without one; `dialects`/`tags` are additive links (this call never removes them). For surgical single-row edits/deletes — one sentence, sense, tag, or dialect — use the dedicated `…/sentences/{id}`, `…/senses/{id}`, `…/tags/{id}`, `…/dialects/{id}` routes (see \"Edits & deletes\"). A row whose content already matches is not rewritten (no sync churn, no history). Patching many entries? Use `POST …/entries/batch-update`. Returns the updated entry in the READ shape (`EntryResponse`).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryPatch"}}}},"responses":{"200":{"description":"The updated nested entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntryResponse"}}}},"400":{"description":"Invalid request"},"404":{"description":"Resource not found"}},"tags":["entries"]},"delete":{"summary":"Delete an entry (cascades senses/junctions)","description":"Tombstones the entry; its senses and their links cascade. Example sentences themselves are kept — only their links to the deleted senses go. Delete a sentence explicitly with `DELETE …/sentences/{sentenceId}`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ result: 'deleted' }"},"404":{"description":"Resource not found"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/batch-delete":{"post":{"summary":"Delete every entry from one bulk import (dry-run + confirm)","description":"Bad-import recovery: removes the whole batch identified by `import_id` (the private tag stamped on every entry of a bulk `POST …/entries`; matched case-insensitively). Two-step by design: call with `\"dry_run\": true` first — it reports `{ count, sample_entry_ids }` and writes NOTHING — then arm the real run by echoing that count back as `confirm_count` (a mismatch with the live count → 409, so a stale script can't delete a re-imported batch). Deletes cascade each entry's senses and links, the emptied private tag is removed too, and orphaned standalone example sentences are deliberately left in place. Unknown `import_id` → 404. Editor+.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["import_id"],"properties":{"import_id":{"type":"string","description":"The `import_id` used on the original bulk entry POST."},"dry_run":{"type":"boolean","description":"true → report the blast radius only, no writes."},"confirm_count":{"type":"integer","description":"Required for a real run: the `count` a dry-run just reported."}}}}}},"responses":{"200":{"description":"{ import_id, count, sample_entry_ids, deleted, tag_deleted? }","content":{"application/json":{"schema":{"type":"object","properties":{"import_id":{"type":"string"},"count":{"type":"integer","description":"Dry-run: entries that WOULD be deleted. Real run: entries actually deleted."},"sample_entry_ids":{"type":"array","items":{"type":"string"},"maxItems":20},"deleted":{"type":"boolean"},"tag_deleted":{"type":"boolean","description":"Real run only: the emptied private import tag was removed."}}}}}},"400":{"description":"Missing import_id, or a real run without confirm_count"},"404":{"description":"No private import tag with that name"},"409":{"description":"confirm_count does not match the live count — re-run dry_run"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/batch-update":{"post":{"summary":"Patch many entries in one request (ordered per-item results, retry-safe)","description":"Up to 1000 entry PATCHes per request — each item is `{ id, …EntryPatch }` with exactly the single `PATCH …/entries/{entryId}` semantics (field-merge, `null` clears, senses upsert by id, additive dialects/tags). Per-item best-effort: one bad item is rolled back and reported, the rest apply. Results come back one per item, in request order: `updated`, `unchanged` (the patch already matched — nothing was written), `not_found`, or `failed` with `error`. RETRY-SAFE: re-send the SAME batch after a timeout, a 5xx or a `halted_error` — items that already landed report `unchanged` and write nothing. To keep that true, every `senses[]` item and every `example_sentences[]` item MUST carry an `id` (mint UUIDs client-side; an id-less one fails that item), and an entry id may appear only once per batch. Editor+.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["entries"],"properties":{"entries":{"type":"array","maxItems":1000,"items":{"allOf":[{"type":"object","required":["id"],"properties":{"id":{"type":"string","description":"The entry to patch."}}},{"$ref":"#/components/schemas/EntryPatch"}]}}}}}}},"responses":{"200":{"description":"Per-item report","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"integer"},"unchanged":{"type":"integer","description":"Items whose patch already matched stored state — nothing written (e.g. the retry of an applied item)."},"not_found":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","description":"One per request item, in order.","items":{"type":"object","properties":{"status":{"type":"string","enum":["updated","unchanged","not_found","failed"]},"entry_id":{"type":"string","description":"Absent only when the item had no usable id."},"error":{"type":"string"}}}},"halted_error":{"type":"string","description":"A transaction-level fault (e.g. the database was locked) stopped the batch; every item not yet committed is `failed` with this error. Re-send the same batch."},"warnings":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"Body is not `{ entries: [...] }`, empty, or over 1000 items"},"401":{"description":"Missing/invalid key"},"403":{"description":"Insufficient role or wrong dictionary"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/{entryId}/tags/{tagId}":{"delete":{"summary":"Unlink a tag from this entry","description":"Removes ONE tag from ONE entry; the tag (and its links to other entries) survives. To delete the tag everywhere use `DELETE …/tags/{tagId}`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}},{"name":"tagId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ result: 'unlinked' }"},"404":{"description":"Tag not linked to this entry"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/{entryId}/dialects/{dialectId}":{"delete":{"summary":"Unlink a dialect from this entry","description":"Removes ONE dialect from ONE entry; the dialect survives globally. To delete it everywhere use `DELETE …/dialects/{dialectId}`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}},{"name":"dialectId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ result: 'unlinked' }"},"404":{"description":"Dialect not linked to this entry"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/sentences":{"post":{"summary":"Create a standalone sentence","description":"Create a first-class sentence without attaching it to a sense or text. Use this for free-standing grammar examples, then attach the returned id to a grammar section (`POST …/grammar/sections/{sectionId}/sentences`) and/or link it to a sense (`PATCH …/entries/{entryId}` with `example_sentences: [{ id }]`). A client-supplied id makes retries idempotent.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SentenceInput"}}}},"responses":{"200":{"description":"{ sentence, created }","content":{"application/json":{"schema":{"type":"object","properties":{"sentence":{"$ref":"#/components/schemas/SentenceFull"},"created":{"type":"boolean"}}}}}},"400":{"description":"Missing content / bad id / unknown source"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/sentences/{sentenceId}":{"get":{"summary":"Read one sentence","description":"Read any sentence by id, including a standalone grammar example.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"sentenceId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ sentence }","content":{"application/json":{"schema":{"type":"object","properties":{"sentence":{"$ref":"#/components/schemas/SentenceFull"}}}}}},"404":{"description":"Resource not found"}},"tags":["entries"]},"patch":{"summary":"Edit one sentence","description":"Field-merge one sentence, whether it is sense-linked, text-owned, or standalone. Returns `{ sentence }`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"sentenceId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SentencePatch"}}}},"responses":{"200":{"description":"The updated sentence","content":{"application/json":{"schema":{"type":"object","properties":{"sentence":{"$ref":"#/components/schemas/SentenceFull"}}}}}},"400":{"description":"Invalid request"},"404":{"description":"Resource not found"}},"tags":["entries"]},"delete":{"summary":"Delete one sentence","description":"Deletes the sentence and its sense/grammar links.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"sentenceId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ result: 'deleted' }"},"404":{"description":"Resource not found"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/senses/{senseId}":{"delete":{"summary":"Delete one sense","description":"Deletes a single sense and its sentence/media links. Refused (400) when it is the entry's ONLY sense — delete the entry instead.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"senseId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ result: 'deleted' }"},"400":{"description":"Can't delete an entry's only sense"},"404":{"description":"Resource not found"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/{entryId}/etymologies":{"get":{"summary":"List an entry's etymology records","description":"The entry's typed etymology records, oldest first (multiple = competing hypotheses). Also embedded on entry reads as `entry.etymologies` when any exist.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ etymologies }","content":{"application/json":{"schema":{"type":"object","properties":{"etymologies":{"type":"array","items":{"$ref":"#/components/schemas/EtymologyFull"}}}}}}},"404":{"description":"Entry not found"}},"tags":["entries"]},"post":{"summary":"Add an etymology record","description":"Adds one typed record; `verdict` is required, everything else optional. Supply your own `id` (uuid): re-POSTing the same id is a safe no-op, so retries never duplicate.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EtymologyInput"}}}},"responses":{"200":{"description":"{ etymology, created }"},"400":{"description":"Bad verdict/confidence, unknown entry, cognate set, or source slug"}},"tags":["entries"]}},"/api/v1/dictionaries/{id}/entries/{entryId}/etymologies/{etymologyId}":{"get":{"summary":"Read an etymology record","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}},{"name":"etymologyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ etymology }","content":{"application/json":{"schema":{"type":"object","properties":{"etymology":{"$ref":"#/components/schemas/EtymologyFull"}}}}}},"404":{"description":"Resource not found"}},"tags":["entries"]},"patch":{"summary":"Update an etymology record","description":"Field-merge: `notes` overlays language keys; `citations` union; scalars replace; `null` clears. `verdict` can change but never to null.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}},{"name":"etymologyId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EtymologyPatch"}}}},"responses":{"200":{"description":"{ etymology }"},"404":{"description":"Resource not found"}},"tags":["entries"]},"delete":{"summary":"Delete an etymology record","description":"Removes the record. The entry (and any linked word family) is untouched.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Dictionary id or url-slug. Must match the dictionary your API key is scoped to."},{"name":"entryId","in":"path","required":true,"schema":{"type":"string"}},{"name":"etymologyId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ result: 'deleted' }"},"404":{"description":"Resource not found"}},"tags":["entries"]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"A Living Dictionaries API key (`ldk_…`) minted on the dictionary's Agents page."}},"schemas":{"MultiString":{"type":"object","description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"LngLat":{"type":"object","required":["longitude","latitude"],"description":"A single WGS-84 geographic point.","properties":{"longitude":{"type":"number","minimum":-180,"maximum":180,"description":"Decimal degrees east, −180…180."},"latitude":{"type":"number","minimum":-90,"maximum":90,"description":"Decimal degrees north, −90…90."}},"example":{"longitude":77.2,"latitude":28.6}},"GeoPoint":{"type":"object","required":["coordinates"],"description":"A single located point (e.g. one elicitation/survey village).","properties":{"coordinates":{"$ref":"#/components/schemas/LngLat"},"label":{"type":"string","description":"Optional display label (e.g. the village name)."},"color":{"type":"string","description":"Optional marker color (any CSS color string)."}}},"GeoRegion":{"type":"object","required":["coordinates"],"description":"A closed area, given as one ring of 3–1000 vertices in drawing order (the ring is drawn as given, so concave outlines are fine; a ring that crosses itself is instead redrawn sorted by angle, which loses concavity). Repeating the first vertex at the end is optional. One outer ring only — no holes; give a multi-part area as several regions.","properties":{"coordinates":{"type":"array","items":{"$ref":"#/components/schemas/LngLat"},"minItems":3,"maxItems":1000,"description":"Ring vertices in drawing order; 3–1000."},"label":{"type":"string"},"color":{"type":"string"}}},"Coordinates":{"type":"object","description":"Where-spoken geometry, `{ points?, regions? }`. On an ENTRY these are the attestation/elicitation location(s) of that specific form; on a DIALECT they are the variety's areal extent (set once on the dialect, not repeated per entry). Longitude/latitude are validated to their real-world ranges; a bad point rejects the write. Limits: ≤100 points, ≤20 regions, each region 3–1000 vertices. On PATCH the whole object is REPLACED — send `null` to clear, or omit the field to leave it untouched.","properties":{"points":{"type":"array","items":{"$ref":"#/components/schemas/GeoPoint"},"maxItems":100},"regions":{"type":"array","items":{"$ref":"#/components/schemas/GeoRegion"},"maxItems":20}},"example":{"points":[{"coordinates":{"longitude":77.2,"latitude":28.6},"label":"Khirsu"}]}},"SentenceInput":{"type":"object","description":"A sentence write/reference. With content fields, creates or field-merges the sentence. Nested under a sense, `{ id: \"<existing sentence UUID>\" }` alone links that existing sentence without rewriting it; an unknown id-only reference fails loudly.","properties":{"id":{"type":"string","format":"uuid","description":"Optional client-generated UUID (any version — deterministic uuid5 ids work well). Supply it to know the id up front (for later edits) and to make writes idempotent — re-sending the same id is a safe no-op. Omit → the server mints one."},"text":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"The sentence in the vernacular."},"translation":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"Translation(s), keyed by gloss-language code."},"tokens":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/SentenceTokenInput"}},"description":"Orthography code → ordered token list (usually just `default`, the vernacular line) — the GOLD alignment from a glossed source, carrying the per-token gloss line + optional morphemes. If the sentence `text` for an orthography is omitted but its tokens are supplied, the server builds `text` by joining the token `form`s with a space — so a rows-only glossed source (aligned `[form, gloss]` rows, no separate vernacular line) imports without pre-assembling the text."},"citations":{"type":"array","items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs — `{ slug }` for bare membership, add `locator` for the page / example number. Each `slug` must already exist in the source registry (create via `POST …/sources`)."},"example_label":{"type":["string","null"],"description":"The author's own example number (e.g. \"(2a)\") for cross-referencing within a grammar."},"discourse_role":{"type":["string","null"],"enum":["storyline","backgrounded","flashback","setting","evaluation","reported_speech",null],"description":"Discourse salience / information role — lets a narrative render foreground vs. background and a grammar section point at the role a particle signals."}}},"SenseInput":{"type":"object","description":"A meaning of the entry.","properties":{"id":{"type":"string","format":"uuid","description":"Optional client-generated UUID (any version — deterministic uuid5 ids work well). Supply it to know the id up front (for later edits) and to make writes idempotent — re-sending the same id is a safe no-op. Omit → the server mints one."},"glosses":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"Short glosses keyed by gloss-language code."},"definition":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"Longer definition(s)."},"parts_of_speech":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"POS abbreviation(s). Send the abbreviation or its full English name — both are matched case-insensitively and stored as the canonical lowercase abbreviation (\"N\" / \"Noun\" → \"n\"). Values outside this list are stored verbatim, so only use custom values for genuinely language-specific categories. Supported: n (noun), n.f (noun, feminine), n.m (noun, masculine), pro (pronoun), v (verb), adj (adjective), adv (adverb), prep (preposition), conj (conjunction), det (determiner), int (interjection), no (number), dem (demonstrative), n pl (plural noun), phr (phrase), p (particle), neg (negative), quan (quantifier), rel (relative), v.aux (auxiliary verb), dem.a (animate demonstrative), dem.i (inanimate demonstrative), dem.loc (locative demonstrative), dim (diminutive), na (animate noun), ni (inanimate noun), pl (plural), pn (prenoun), pv (preverb), pn/v (prenoun / preverb), pro.a (animate pronoun), pro.i (inanimate pronoun), redup (reduplication), refl (reflexive), vai (animate intransitive verb), vai/ii (animate / inanimate intransitive verb), vii (inanimate intransitive verb), vta (transitive animate verb), vti (transitive inanimate verb), imp (imperative/command marker), pref (prefix), punc (punctuation), suff (suffix), vt (transitive verb), vi (intransitive verb), comp (complement(izer)), cpd (compound), pple (participle), sent (sentence), pr.n (proper noun), np (noun phrase), vp (verb phrase), ap (adjective phrase), adv.p (adverb phrase), q (question marker), ger (gerund), art (article), post (postposition), e.w (entry word / discourse particle), m.w (measure word), msd (masdar), idf (idafa), v.prdg (verbal paradigm), acr (acronym), cntr (contraction), num (numeral), act.v (active verb), stat.v (stative verb), caus.v (causative verb), v.suff (verb suffix), n.suff (noun suffix), n.pref (noun prefix), v.pref (verb prefix), ideo (ideophone (onomatoepia / expressives)), clf (classifier), tns (tense marker), asp (aspect marker), mod (mood marker), n.cls (noun class marker), quot (quotative), adp (adposition), def (definite article), indf (indefinite article), indfpro (indefinite pronoun), pers (personal pronoun), poss (possessive pronoun), reflpro (reflexive pronoun), relpro (relative pronoun), vd (ditransitive verb), va (ambitransitive), obj (object marker)."},"semantic_domains":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Semantic domain keys."},"write_in_semantic_domains":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Free-text semantic domains."},"noun_class":{"type":"string"},"plural_form":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},"variant":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},"citations":{"type":"array","items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs for THIS sense — per-sense provenance when an entry's senses are merged from several sources. `{ slug }` for bare membership, add `locator` for a page / example number; each slug must already exist (create via `POST …/sources`)."},"example_sentences":{"type":"array","items":{"$ref":"#/components/schemas/SentenceInput"},"description":"Sentence content to create/upsert, or `{ id }` alone to link an existing sentence. Re-linking is idempotent; an unknown id-only reference fails."}}},"EntryInput":{"type":"object","required":["lexeme"],"description":"A dictionary entry (headword) with nested senses.","properties":{"id":{"type":"string","format":"uuid","description":"Optional client-generated UUID (any version — deterministic uuid5 ids work well) — THE idempotency key. Generate it yourself and a re-POST of the same entry is a safe no-op (`status: \"exists\"`) instead of a duplicate, and you already know the id for later `PATCH …/entries/{id}` edits (no round-trip to discover it). Omit → the server mints one. Must be a valid UUID if provided."},"lexeme":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"The headword. Required."},"homograph":{"type":"string","description":"Homograph number for identically-spelled headwords — \"1\", \"2\" (some sources use \"a\"/\"b\"). Rendered as a superscript after the lexeme, so deliberate homographs are distinguishable from accidental duplicates. Orthogonal to the `homophone` relationship type (differently-spelled words that sound alike)."},"phonetic":{"type":"string"},"interlinearization":{"type":"string"},"morphology":{"type":"string"},"notes":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"Rich text stored as MARKDOWN (headings/bold/lists/links) — write markdown, not HTML."},"linguistic_history":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},"citations":{"type":"array","items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs — `{ slug }` for bare membership, add `locator` for the page / example number this entry came from (always known for scanned dictionaries). Each slug must already exist in this dictionary's registry (create via `POST …/sources`); an unknown slug rejects the write."},"scientific_names":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"elicitation_id":{"type":"string","description":"Source-side stable id for word-list/elicitation ordering. Persisted and queryable via `?elicitation_id=`; use it as a server-recoverable dedupe key ONLY if your source id is genuinely elicitation data — for generic idempotency, supply your own `id` instead."},"tone_class":{"type":"string","description":"Analytic tone-category label (a small per-language set, e.g. \"1\", \"A\", \"high\") — distinct from the phonetic tone spelled in the orthography. Free text; keep the label set consistent within a dictionary."},"coordinates":{"oneOf":[{"$ref":"#/components/schemas/Coordinates"},{"type":"null"}],"description":"Where-spoken geometry for this form: the attestation/elicitation point(s) (and/or region[s]). See the `Coordinates` schema."},"dialects":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Dialect names — found-or-created on this dictionary."},"tags":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Tag names — found-or-created."},"review":{"anyOf":[{"$ref":"#/components/schemas/EntryReview"},{"type":"null"}],"description":"Editor-only \"needs review\" flag `{ category, note }` — set it for anything you had to guess/salvage so a human reviews it. Never shown to the public."},"senses":{"type":"array","items":{"$ref":"#/components/schemas/SenseInput"},"description":"Defaults to one empty sense if omitted."}}},"SensePatch":{"type":"object","description":"A sense within a PATCH — a true upsert by client id. With an `id` already on this entry → field-merge that sense; with an unknown `id` (or none) → create the sense WITH that id (deterministic import ids keep addressing the same sense across re-syncs). An `id` belonging to a different entry is a 400. On PATCH, provenance MERGES and language keys overlay: `citations` union with what is there, sending one gloss language leaves the others untouched, `\"\"` drops one language, `null` clears a field (every nullable property below). `parts_of_speech`/`semantic_domains` are replaced outright. Example sentences upsert by id; `{ id }` alone links an existing sentence without rewriting it, existing links are not duplicated, and an unknown id-only reference is a 400.","properties":{"id":{"type":"string","format":"uuid","description":"The sense to field-merge; an unknown id creates the sense with that id."},"glosses":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}],"description":"Short glosses keyed by gloss-language code."},"definition":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}],"description":"Longer definition(s)."},"parts_of_speech":{"anyOf":[{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},{"type":"null"}],"description":"POS abbreviation(s). Send the abbreviation or its full English name — both are matched case-insensitively and stored as the canonical lowercase abbreviation (\"N\" / \"Noun\" → \"n\"). Values outside this list are stored verbatim, so only use custom values for genuinely language-specific categories. Supported: n (noun), n.f (noun, feminine), n.m (noun, masculine), pro (pronoun), v (verb), adj (adjective), adv (adverb), prep (preposition), conj (conjunction), det (determiner), int (interjection), no (number), dem (demonstrative), n pl (plural noun), phr (phrase), p (particle), neg (negative), quan (quantifier), rel (relative), v.aux (auxiliary verb), dem.a (animate demonstrative), dem.i (inanimate demonstrative), dem.loc (locative demonstrative), dim (diminutive), na (animate noun), ni (inanimate noun), pl (plural), pn (prenoun), pv (preverb), pn/v (prenoun / preverb), pro.a (animate pronoun), pro.i (inanimate pronoun), redup (reduplication), refl (reflexive), vai (animate intransitive verb), vai/ii (animate / inanimate intransitive verb), vii (inanimate intransitive verb), vta (transitive animate verb), vti (transitive inanimate verb), imp (imperative/command marker), pref (prefix), punc (punctuation), suff (suffix), vt (transitive verb), vi (intransitive verb), comp (complement(izer)), cpd (compound), pple (participle), sent (sentence), pr.n (proper noun), np (noun phrase), vp (verb phrase), ap (adjective phrase), adv.p (adverb phrase), q (question marker), ger (gerund), art (article), post (postposition), e.w (entry word / discourse particle), m.w (measure word), msd (masdar), idf (idafa), v.prdg (verbal paradigm), acr (acronym), cntr (contraction), num (numeral), act.v (active verb), stat.v (stative verb), caus.v (causative verb), v.suff (verb suffix), n.suff (noun suffix), n.pref (noun prefix), v.pref (verb prefix), ideo (ideophone (onomatoepia / expressives)), clf (classifier), tns (tense marker), asp (aspect marker), mod (mood marker), n.cls (noun class marker), quot (quotative), adp (adposition), def (definite article), indf (indefinite article), indfpro (indefinite pronoun), pers (personal pronoun), poss (possessive pronoun), reflpro (reflexive pronoun), relpro (relative pronoun), vd (ditransitive verb), va (ambitransitive), obj (object marker)."},"semantic_domains":{"anyOf":[{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},{"type":"null"}],"description":"Semantic domain keys."},"write_in_semantic_domains":{"anyOf":[{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},{"type":"null"}],"description":"Free-text semantic domains."},"noun_class":{"type":["string","null"]},"plural_form":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}]},"variant":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}]},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs for THIS sense — per-sense provenance when an entry's senses are merged from several sources. `{ slug }` for bare membership, add `locator` for a page / example number; each slug must already exist (create via `POST …/sources`)."},"example_sentences":{"type":"array","items":{"$ref":"#/components/schemas/SentenceInput"},"description":"Sentence content to create/upsert, or `{ id }` alone to link an existing sentence. Re-linking is idempotent; an unknown id-only reference fails."}}},"SentencePatch":{"type":"object","description":"Field-merge for one sentence (`PATCH …/sentences/{id}`) — works on both an entry's example sentence and a text-sentence. Omitted fields stay. `text`/`translation` overlay language keys and `citations` MERGE, so patching one language or one source never deletes another; `null` clears a field.","properties":{"text":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}],"description":"The sentence in the vernacular."},"translation":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}],"description":"Translation(s), keyed by gloss-language code."},"ends_paragraph":{"type":"boolean","description":"For a text-sentence: whether a paragraph break follows it."},"tokens":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/SentenceTokenInput"}},"description":"Orthography code → ordered token list (usually just `default`, the vernacular line) — the GOLD alignment from a glossed source, carrying the per-token gloss line + optional morphemes. If the sentence `text` for an orthography is omitted but its tokens are supplied, the server builds `text` by joining the token `form`s with a space — so a rows-only glossed source (aligned `[form, gloss]` rows, no separate vernacular line) imports without pre-assembling the text."},"citations":{"type":"array","items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs — `{ slug }` for bare membership, add `locator` for the page / example number. Each `slug` must already exist in the source registry (create via `POST …/sources`)."},"example_label":{"type":["string","null"],"description":"The author's own example number (e.g. \"(2a)\") for cross-referencing within a grammar."},"discourse_role":{"type":["string","null"],"enum":["storyline","backgrounded","flashback","setting","evaluation","reported_speech",null],"description":"Discourse salience / information role — lets a narrative render foreground vs. background and a grammar section point at the role a particle signals."}}},"EntryPatch":{"type":"object","description":"Partial entry update. Provided fields overwrite; omitted ones are untouched; `null` clears any nullable field (every one except `lexeme`, `dialects`, `tags`, `senses`). `dialects`/`tags` are ADDITIVE links. `senses` upsert by client id (unknown id → created with that id — see SensePatch). Within a sense, `example_sentences: [{ id }]` links an existing sentence by reference without copying its content.","properties":{"lexeme":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},"homograph":{"type":["string","null"],"description":"Homograph number (\"1\", \"2\", …); `\"\"` or `null` clears."},"phonetic":{"type":["string","null"]},"interlinearization":{"type":["string","null"]},"morphology":{"type":["string","null"]},"notes":{"anyOf":[{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}]},{"type":"null"}],"description":"Rich text stored as MARKDOWN (headings/bold/lists/links) — write markdown, not HTML. On PATCH, language keys MERGE: sending one language leaves the others untouched, `\"\"` drops just that language, and `null` clears the field."},"linguistic_history":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"},{"type":"null"}]},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs (`{ slug, locator? }`). On PATCH these MERGE with the citations already on the row (deduped by slug+locator) so you cannot delete another contributor's attribution; send `null` to clear them."},"scientific_names":{"anyOf":[{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},{"type":"null"}],"description":"Whole-array replace — a scientific name states what the word IS, so a PATCH says the new list outright. `null` clears."},"elicitation_id":{"type":["string","null"]},"tone_class":{"type":["string","null"],"description":"Analytic tone-category label — `\"\"`/`null` clears it; omit → untouched."},"coordinates":{"oneOf":[{"$ref":"#/components/schemas/Coordinates"},{"type":"null"}],"description":"Whole-object replace: `{ points?, regions? }` overwrites; `null` clears; omit → untouched."},"dialects":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ADDITIVE dialect links (found-or-created). Not clearable here — unlink with `DELETE …/entries/{entryId}/dialects/{dialectId}`."},"tags":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"ADDITIVE tag links (found-or-created). Not clearable here — unlink with `DELETE …/entries/{entryId}/tags/{tagId}`."},"review":{"anyOf":[{"$ref":"#/components/schemas/EntryReview"},{"type":"null"}],"description":"Editor-only \"needs review\" flag — `{ category, note }` sets/replaces it, `null` clears it (\"Resolve\"), omit → untouched."},"senses":{"type":"array","items":{"$ref":"#/components/schemas/SensePatch"}}}},"EntryWriteResult":{"type":"object","properties":{"status":{"type":"string","enum":["created","exists","updated","failed"],"description":"`created` = new entry written; `exists` = a client-supplied `id` already existed, so this item was skipped (idempotent no-op — edit via PATCH); `failed` = see `error`."},"entry_id":{"type":"string","description":"The entry id (the client-supplied one when given). Absent only on a pre-id failure."},"sense_ids":{"type":"array","items":{"type":"string"}},"error":{"type":"string","description":"Why this item failed (absent on success)."}}},"EntriesWriteResponse":{"type":"object","properties":{"created":{"type":"integer"},"skipped":{"type":"integer","description":"Items skipped because their client-supplied `id` already existed."},"updated":{"type":"integer"},"failed":{"type":"integer"},"results":{"type":"array","items":{"$ref":"#/components/schemas/EntryWriteResult"},"description":"One per input entry, in order."},"warnings":{"type":"array","items":{"type":"string"},"description":"Present only when the request used a deprecated input shape (every write endpoint does this) — the write still succeeded; fix the shape."}}},"SenseSummary":{"type":"object","description":"A sense's meaning fields — attached to list rows only when `?include=senses`.","properties":{"id":{"type":"string"},"glosses":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"definition":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"parts_of_speech":{"type":["array","null"],"items":{"type":"string"}},"semantic_domains":{"type":["array","null"],"items":{"type":"string"}}}},"EntrySummary":{"type":"object","properties":{"id":{"type":"string"},"lexeme":{"$ref":"#/components/schemas/MultiString"},"homograph":{"type":["string","null"]},"phonetic":{"type":["string","null"]},"elicitation_id":{"type":["string","null"]},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Entry-level source refs — always included, so provenance checks don't need per-entry detail fetches."},"updated_at":{"type":"string","format":"date-time"},"senses":{"type":"array","items":{"$ref":"#/components/schemas/SenseSummary"},"description":"Present only when the request passes `?include=senses`."}}},"EntriesListResponse":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/EntrySummary"}},"has_more":{"type":"boolean","description":"True when more rows exist past this page — bump `offset` by `limit` and re-request. Default order is `updated_at` ASC, so paginating to the end gives you every entry (handy for a live count or verifying an import)."}}},"SentenceFull":{"type":"object","description":"A stored example sentence (read shape). You POST these under `senses[].example_sentences`; they are returned here under `senses[].sentences`. Edit or remove one by its `id` via `PATCH`/`DELETE …/sentences/{id}`.","properties":{"id":{"type":"string"},"text":{"$ref":"#/components/schemas/MultiString"},"translation":{"$ref":"#/components/schemas/MultiString"},"text_id":{"type":["string","null"],"description":"Set when the sentence belongs to a longer connected `text`; `null` for a standalone example sentence (the usual case for imports)."},"sort_key":{"type":["string","null"],"description":"Fractional ordering index within its `text_id`; `null` for standalone example sentences."},"ends_paragraph":{"type":["integer","null"],"description":"1 when a paragraph break follows this sentence within a `text`; otherwise `null`."},"tokens":{"type":["object","null"],"additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/SentenceTokenFull"}},"description":"Stored interlinear tokens per orthography (the shared index for the gloss line, word→entry taps, and karaoke timings)."},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs (`{ slug, locator? }`)."},"example_label":{"type":["string","null"],"description":"The author's own example number."},"discourse_role":{"type":["string","null"],"description":"Discourse salience / information role."},"updated_at":{"type":"string","format":"date-time"}}},"SenseFull":{"type":"object","description":"A stored sense (read shape). Mirrors `SenseInput` plus a server-assigned `id`; example sentences come back under `sentences` (not `example_sentences`). Media arrays (`photos`/`videos`) are read-only here — write them via `POST …/senses/{senseId}/photos` / `…/videos`.","properties":{"id":{"type":"string"},"glosses":{"$ref":"#/components/schemas/MultiString"},"definition":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"parts_of_speech":{"type":["array","null"],"items":{"type":"string"}},"semantic_domains":{"type":["array","null"],"items":{"type":"string"}},"write_in_semantic_domains":{"type":["array","null"],"items":{"type":"string"}},"noun_class":{"type":["string","null"]},"plural_form":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"variant":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Per-sense source refs."},"sentences":{"type":"array","items":{"$ref":"#/components/schemas/SentenceFull"},"description":"The sense's example sentences. Present only when sentences exist. (Input field name is `example_sentences`.)"},"photos":{"type":"array","items":{"$ref":"#/components/schemas/PhotoMedia"},"description":"Present only when photos exist. Write via `POST …/senses/{senseId}/photos`."},"videos":{"type":"array","items":{"$ref":"#/components/schemas/VideoMedia"},"description":"Present only when videos exist. Write via `POST …/senses/{senseId}/videos`."},"updated_at":{"type":"string","format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"EntryReview":{"type":"object","required":["category","note"],"description":"EDITOR-ONLY \"needs review\" task. Queues an entry for a human reviewer WITHOUT showing the public — it never appears in the reader UI or search for non-editors (though it does sync in the public snapshot, same as a `private` tag). Use it only for a question that remains unresolved after import cleanup, not as an audit log of deterministic transformations already completed. The note must be answerable from the entry page alone; structured source provenance belongs in the entry's `citations` and is rendered separately under collapsed Source details. A reviewer clears it (\"Resolve\"). On READ it is present under `entry.main.review` ONLY when the caller is an editor.","properties":{"category":{"type":"string","description":"Free bucket label for triage — drives the entries-list \"Needs review\" category facet. Reuse a small consistent vocabulary within an import, e.g. `truncated`, `headword_in_gloss`, `language_split`, `uncertain_gloss`, `dropped_text`, `missing_gloss`, `other`."},"note":{"type":"string","description":"A self-contained, plain-language question shown verbatim in the editor-only banner. Name the sense when useful; include the complete original and imported/omitted values needed to decide and end with a concrete question. Never use code paths (`glosses.gn`), internal flag/parser jargon, source-line tracebacks, or instructions to inspect the source file. When `comparisons` carry the competing values, keep the note to the framing sentence plus the question — do NOT repeat the values in prose."},"comparisons":{"type":"array","items":{"$ref":"#/components/schemas/EntryReviewComparison"},"description":"Competing values for one or more fields. The banner renders each pair with the differing spans highlighted (so the reviewer sees the difference instead of hunting for it) and, when `apply` is set, offers one-click \"Use this\" on each version."}}},"EntryReviewComparison":{"type":"object","required":["field","a","b"],"description":"One disputed field and its two versions. Use it whenever a review exists because two sources (or two printings of one source) disagree about a value.","properties":{"field":{"type":"string","description":"Human label for the disputed field, e.g. `Definition`, `Part of speech`, `Pronunciation guide`."},"a":{"$ref":"#/components/schemas/EntryReviewComparisonSide"},"b":{"$ref":"#/components/schemas/EntryReviewComparisonSide"},"apply":{"$ref":"#/components/schemas/EntryReviewApply"}}},"EntryReviewComparisonSide":{"type":"object","required":["value"],"properties":{"label":{"type":"string","description":"Where this version comes from, e.g. `Main dictionary (p62)`."},"value":{"type":"string","description":"The value exactly as that source prints it."}}},"EntryReviewApply":{"type":"object","required":["target"],"description":"Where a chosen version gets written when the reviewer clicks \"Use this\". Omit it when the two versions are not directly writable (e.g. the two halves may be different words entirely).","properties":{"target":{"type":"string","enum":["entry.lexeme","entry.phonetic","sense.glosses","sense.definition"]},"sense_id":{"type":"string","description":"Required for `sense.*` targets."},"key":{"type":"string","description":"Orthography code for `entry.lexeme` (e.g. `default`); language code for `sense.glosses` / `sense.definition` (e.g. `en`). Unused by `entry.phonetic`."}}},"EntryMain":{"type":"object","description":"The entry's top-level scalar fields. These are exactly the fields you POST at the top level of `EntryInput` (lexeme, phonetic, notes, citations, …) — on READ they live here, nested under `entry.main`.","properties":{"lexeme":{"$ref":"#/components/schemas/MultiString"},"homograph":{"type":["string","null"]},"phonetic":{"type":["string","null"]},"interlinearization":{"type":["string","null"]},"morphology":{"type":["string","null"]},"notes":{"type":["object","null"],"description":"Rich text as MARKDOWN.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"linguistic_history":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"}},"scientific_names":{"type":["array","null"],"items":{"type":"string"}},"elicitation_id":{"type":["string","null"]},"tone_class":{"type":["string","null"],"description":"Analytic tone-category label, when set."},"coordinates":{"oneOf":[{"$ref":"#/components/schemas/Coordinates"},{"type":"null"}],"description":"Where-spoken geometry (attestation/elicitation location) for this form, when set."},"review":{"anyOf":[{"$ref":"#/components/schemas/EntryReview"},{"type":"null"}],"description":"Editor-only \"needs review\" flag — present only for editor callers."}}},"EntryFull":{"type":"object","description":"A fully-assembled entry (read shape) — the read-side mirror of `EntryInput`. ASYMMETRY: scalars POSTed at the top level are read back under `main`; `senses[].example_sentences` are read back as `senses[].sentences`.","properties":{"id":{"type":"string"},"main":{"$ref":"#/components/schemas/EntryMain"},"senses":{"type":"array","items":{"$ref":"#/components/schemas/SenseFull"}},"tags":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"private":{"type":["boolean","null"]},"updated_at":{"type":"string","format":"date-time"}}},"description":"Entry-level tags (includes the `import_id` private tag from a bulk import). Present only when tags exist."},"dialects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"$ref":"#/components/schemas/MultiString"},"coordinates":{"oneOf":[{"$ref":"#/components/schemas/Coordinates"},{"type":"null"}],"description":"The dialect's areal-extent geometry, when set (managed via the `…/dialects` endpoints, not entry payloads)."},"updated_at":{"type":"string","format":"date-time"}}},"description":"Present only when dialects exist."},"audios":{"type":"array","items":{"$ref":"#/components/schemas/AudioMedia"},"description":"Present only when audio exists. Write via `POST …/entries/{entryId}/audio`."},"text_audio":{"type":"array","items":{"$ref":"#/components/schemas/TextAudioClip"},"description":"Up to 3 word clips BORROWED from timed text/sentence recordings (a token linked to this entry inside audio that carries `timings`). Derived on read, never stored — attach a real recording via `POST …/entries/{entryId}/audio`; this field is the fallback the UI plays when there is none. Present only when such clips exist."},"cognate_sets":{"type":"array","items":{"$ref":"#/components/schemas/CognateSetSummary"},"description":"Word families this entry belongs to (compact summaries). Present only when the entry is a member of at least one set; manage memberships via the `…/cognate-sets` endpoints (tag: cognate-sets)."},"etymologies":{"type":"array","items":{"$ref":"#/components/schemas/EtymologyFull"},"description":"The entry's typed etymology records (multiple = competing hypotheses). Present only when at least one record exists; manage via the `…/entries/{entryId}/etymologies` endpoints. The free-prose `main.linguistic_history` is the untyped companion."},"updated_at":{"type":"string","format":"date-time"}}},"EntryResponse":{"type":"object","description":"Returned by `GET` and `PATCH /entries/{id}`.","required":["entry"],"properties":{"entry":{"$ref":"#/components/schemas/EntryFull"}}},"SpeakerBrief":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"AudioMedia":{"type":"object","description":"A stored audio recording (the pronunciation/utterance). Attaches to an entry, sentence, or text. Requires attribution: a speaker and/or a registry `source`.","properties":{"id":{"type":"string"},"storage_path":{"type":"string"},"download_url":{"type":"string","description":"Absolute URL for the audio bytes (`GET …/media/{storage_path}`, same Bearer auth, 302-redirects to storage). Present in text reads."},"source":{"type":["string","null"],"description":"A sources-registry slug (see `GET …/sources`) — the speaker-less attribution path."},"timings":{"type":["object","null"],"additionalProperties":{"type":"string"},"description":"Karaoke word-timings: a map of sentence id → compact timing string `\"offset,duration|offset,duration|…\"` (milliseconds). Entries align 1:1 with that sentence's default-orthography tokens; each offset is relative to the END of the previous timed token (chainable across sentences for text-level audio); an empty entry marks an untimed token (punctuation).","example":{"sentence-uuid-1":"0,320|40,280|","sentence-uuid-2":"120,300|"}},"entry_id":{"type":["string","null"]},"sentence_id":{"type":["string","null"]},"text_id":{"type":["string","null"]},"speakers":{"type":"array","items":{"$ref":"#/components/schemas/SpeakerBrief"},"description":"Present only when a speaker is attached."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TextAudioClip":{"type":"object","description":"A word's audio borrowed from a timed text/sentence recording: the span of the token linked to the entry. Play `storage_path` from `start_ms` to `end_ms` (a media fragment `#t=start,end` works). Confirmed links sort before auto matches.","properties":{"audio_id":{"type":"string"},"storage_path":{"type":"string"},"start_ms":{"type":"integer"},"end_ms":{"type":"integer"},"sentence_id":{"type":"string"},"text_id":{"type":["string","null"],"description":"null for sentence-level audio."},"text_title":{"$ref":"#/components/schemas/MultiString"},"speaker_name":{"type":"string"},"status":{"type":"string","enum":["auto","confirmed"],"description":"Whether a human confirmed the word→entry link the clip rests on."}},"required":["audio_id","storage_path","start_ms","end_ms","sentence_id","text_id","status"]},"PhotoMedia":{"type":"object","description":"A stored photo. Attaches to a sense or a sentence. `source` + `photographer` are the free-text attribution shown under the image (there is no separate caption field; unlike audio/video, a photo `source` is NOT a registry slug). HEIC/HEIF uploads are rejected with 415 — convert to JPEG/PNG/WebP first (e.g. `magick photo.heic photo.jpg`). EXIF GPS + capture time are read from the bytes automatically; coordinates are stored at village-level precision (2 decimals, ~1.1 km) for contributor privacy.","properties":{"id":{"type":"string"},"storage_path":{"type":"string"},"source":{"type":["string","null"],"description":"Free-text attribution/caption prose."},"photographer":{"type":["string","null"]},"latitude":{"type":["number","null"],"description":"EXIF GPS, blunted to 2 decimals (~1.1 km, village-level) on ingest."},"longitude":{"type":["number","null"],"description":"EXIF GPS, blunted to 2 decimals (~1.1 km, village-level) on ingest."},"taken_at":{"type":["string","null"],"format":"date-time","description":"EXIF capture timestamp."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"VideoMedia":{"type":"object","description":"A stored or hosted video. Attaches to a sense, sentence, or text. Either `storage_path` (uploaded bytes) OR `hosted_elsewhere` (YouTube/Vimeo) is set. Requires attribution: a speaker and/or a registry `source`.","properties":{"id":{"type":"string"},"storage_path":{"type":["string","null"]},"hosted_elsewhere":{"type":["object","null"],"required":["type","video_id"],"description":"A video hosted on YouTube/Vimeo — no bytes are stored. Alternatively send a raw `hosted_url` (a watch/share URL) and the server parses it into this shape.","properties":{"type":{"type":"string","enum":["youtube","vimeo"]},"video_id":{"type":"string","description":"Provider id (YouTube watch id / Vimeo numeric id)."},"start_at_seconds":{"type":["integer","null"]}}},"hosted_metadata":{"type":["object","null"],"description":"Best-effort provider metadata cached when a hosted video is attached. Metadata unavailability never prevents the reference from being saved.","properties":{"title":{"type":"string"},"description":{"type":"string"},"thumbnail_url":{"type":"string","format":"uri"},"duration_seconds":{"type":"number"}}},"source":{"type":["string","null"],"description":"A sources-registry slug (see `GET …/sources`) — the speaker-less attribution path."},"videographer":{"type":["string","null"]},"text_id":{"type":["string","null"]},"speakers":{"type":"array","items":{"$ref":"#/components/schemas/SpeakerBrief"},"description":"Present only when a speaker is attached."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"SourceCitation":{"type":"object","required":["slug"],"description":"A source reference WITH a citation locus (page / example number, e.g. \"1981:31\"). A bare slug string is still accepted where a source ref is expected; this richer form preserves scholarly provenance.","properties":{"slug":{"type":"string","description":"An existing source-registry slug."},"locator":{"type":"string","description":"Page / example number / \"as quoted from X 1981:31\" note."}}},"Morpheme":{"type":"object","required":["form"],"description":"Optional word-INTERNAL segmentation of a token, for polysynthetic/agglutinative data (isolating languages need only the token-level `gloss`).","properties":{"form":{"type":"string","description":"The morpheme's surface form."},"gloss":{"type":"object","description":"Aligned gloss for this morpheme, per analysis language.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"entry_id":{"type":["string","null"],"description":"Optional link to a headword (independent of `gloss`)."},"candidates":{"type":"array","items":{"type":"string"},"description":"Read-mostly: homograph entry ids the affix-aware matcher could not choose between (`-k¹ -k² -k³`). Resolve by writing `entry_id` via `segment`."},"separator":{"type":"string","enum":["-","=","~","."],"description":"Leipzig boundary preceding this morpheme: `-` affix, `=` clitic, `~` reduplication, `.` one-form-many-glosses. Omit for the root/first morpheme. Lets the gloss line render in faithful Leipzig notation."}}},"SentenceTokenInput":{"type":"object","required":["form"],"description":"A writable interlinear token — the GOLD alignment from a glossed source. `start`/`end` are OPTIONAL on write: omit them and the server derives offsets by walking the ORDERED `form`s against the sentence `text` with a LEFT-TO-RIGHT CURSOR that consumes each match in turn (a global search would collide — ~28% of real sentences repeat a form). Derivation therefore requires each `form` to be an exact substring of `text`, in order: keep surface forms byte-identical to the text (do not strip footnote/tone/transcription artifacts from the form but not the text, or vice-versa). On read, offsets are always present.","properties":{"form":{"type":"string","description":"Surface form exactly as it appears in the sentence text (byte-identical, so offset derivation can locate it)."},"start":{"type":"integer","description":"Char offset into the orthography's text. Optional on write (derived if omitted)."},"end":{"type":"integer","description":"Char offset, exclusive. Optional on write."},"gloss":{"type":"object","description":"The aligned interlinear gloss — the Leipzig gloss line, per analysis language. CONVENTION (locked): store language-neutral grammatical category codes (`3PL`, `PFV`, `CLF`) under the reserved `default` key, and per-language LEXICAL glosses (`tiger`, `虎`) under their language codes (`en`, `zh`). A reader on gloss-language X sees `gloss[X] ?? gloss.default`, so a neutral code kept under `default` survives every gloss-language switch (storing it under `en` would make it vanish for a `zh` reader). Independent of `entry_id` (grammatical morphemes/portmanteaux often have no headword but must still be glossable). A legend code found ANYWHERE in a gloss cell (matched as a substring, so portmanteaux like `eat PFV` / `can/ATT` still highlight) renders SMALL CAPS + tap-to-expand automatically — so no per-token \"grammatical?\" flag is needed.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"entry_id":{"type":["string","null"],"description":"Optional link to the dictionary entry for this token (independent of, and optional to, `gloss`). Multi-word→one-gloss = one token spanning the char range."},"sense_id":{"type":["string","null"]},"candidates":{"type":"array","items":{"type":"string"},"description":"Shortlist of candidate entry ids for an AMBIGUOUS match (homographs) — write it instead of dropping the link. In orthographies where spelling neutralizes a contrast (tone, length, stress) the highest-frequency words are exactly the ambiguous ones, so a matcher that can only write `entry_id` or nothing loses the words readers tap most. Candidates surface in the `ambiguous` facet of the suggestions queue for a human to resolve; leave `status` absent alongside them. Ids already resolved by `entry_id` are dropped."},"morphemes":{"type":"array","items":{"$ref":"#/components/schemas/Morpheme"},"description":"Optional word-internal segmentation."},"status":{"type":"string","enum":["auto","confirmed","ignored"],"description":"Match state. OMIT IT to mean UNMATCHED — that absent state is what keeps a token reachable by the auto-matcher and by the suggestions review queue, so never stamp `confirmed` on a token no human confirmed. Omitted, a token with a resolved `entry_id`/`sense_id` (or a linked morpheme) defaults to `confirmed`; a bare, gloss-only, or candidates-only token stays statusless — a gloss is metadata, not a link, and the matcher links glossed words while keeping the gloss. `auto` = machine-matched, unconfirmed. `ignored` = punctuation (keeps offset/timing arrays aligned)."}}},"SentenceTokenFull":{"type":"object","description":"A stored interlinear token (read shape). The default-orthography token list is the shared index that the gloss line, word→entry taps, AND `audio.timings` karaoke all align to 1:1.","properties":{"form":{"type":"string"},"start":{"type":"integer"},"end":{"type":"integer"},"gloss":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"entry_id":{"type":["string","null"]},"sense_id":{"type":["string","null"]},"candidates":{"type":"array","items":{"type":"string"},"description":"Candidate entry ids when the match is ambiguous (homographs) and no single `entry_id` is resolved yet."},"morphemes":{"type":"array","items":{"$ref":"#/components/schemas/Morpheme"}},"status":{"type":["string","null"],"description":"Absent = unmatched (still reachable by the matcher + review queue)."}}},"CognateSetSummary":{"type":"object","description":"The compact set shape embedded on entry reads (`entry.cognate_sets`, present only when the entry belongs to at least one set).","properties":{"id":{"type":"string"},"reconstruction":{"type":["string","null"]},"proto_level":{"type":["string","null"]},"gloss":{"$ref":"#/components/schemas/MultiString"},"member_count":{"type":"integer"}}},"EtymologyInput":{"type":"object","required":["verdict"],"description":"One TYPED etymology record for an entry — the queryable companion to the free-prose `linguistic_history` MultiString (which stays as-is). Zero-to-many per entry; multiple records are COMPETING HYPOTHESES. `verdict` gates which side is meaningful but nothing else is hard-required: an `unknown` verdict with a note is a legitimate record (\"origin unclear, see discussion\"). A `loan` may still carry `reconstruction_level`/`reconstruction` when the word was borrowed FROM a reconstructed stage.","properties":{"id":{"type":"string","format":"uuid","description":"Optional client-generated UUID (any version — deterministic uuid5 ids work well). Supply it to know the id up front (for later edits) and to make writes idempotent — re-sending the same id is a safe no-op. Omit → the server mints one."},"verdict":{"type":"string","enum":["inherited","loan","unknown"]},"reconstruction_level":{"type":["string","null"],"description":"The reconstruction level, e.g. \"Proto-Riverine\". Free text; keep one consistent spelling per level within a dictionary."},"reconstruction":{"type":["string","null"],"description":"The ancestor form WITHOUT the leading asterisk (display adds it)."},"cognate_set_id":{"type":["string","null"],"description":"Point at the word family (`…/cognate-sets`) instead of / in addition to carrying a standalone reconstruction. Must be an existing set."},"donor_language":{"type":["string","null"],"description":"The donor language name (loan path). Free text."},"donor_form":{"type":["string","null"],"description":"The form in the donor language."},"stratum":{"type":["string","null"],"description":"Loan stratum / layer label. Free text; keep one consistent spelling per stratum within a dictionary."},"confidence":{"type":["string","null"],"enum":["certain","probable","possible",null]},"notes":{"oneOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"}],"description":"Discussion as MARKDOWN, per analysis language."},"citations":{"type":"array","items":{"$ref":"#/components/schemas/SourceCitation"},"description":"Source refs — where this etymology is asserted: `{ slug }` for bare membership, add `locator` for the page / item number. Each slug must already exist (`POST …/sources` first)."}},"example":{"verdict":"loan","donor_language":"Lakeshore","donor_form":"kelu","stratum":"early trade layer","confidence":"probable","notes":"Borrowed with the introduction of net fishing."}},"EtymologyPatch":{"type":"object","description":"Field-merge edit of an etymology record: `notes` overlays language keys, `citations` UNION with what is already there (attribution accumulates), scalars replace. Send `null` to clear a field. `verdict` can change (a hypothesis gets settled) but never to null — the record's other fields are preserved, so a verdict flip keeps the evidence it supersedes.","properties":{"verdict":{"type":"string","enum":["inherited","loan","unknown"]},"reconstruction_level":{"type":["string","null"]},"reconstruction":{"type":["string","null"]},"cognate_set_id":{"type":["string","null"]},"donor_language":{"type":["string","null"]},"donor_form":{"type":["string","null"]},"stratum":{"type":["string","null"]},"confidence":{"type":["string","null"],"enum":["certain","probable","possible",null]},"notes":{"anyOf":[{"type":"string"},{"$ref":"#/components/schemas/MultiString"},{"type":"null"}]},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"}}}},"EtymologyFull":{"type":"object","description":"A typed etymology record (read shape).","properties":{"id":{"type":"string"},"entry_id":{"type":"string"},"verdict":{"type":"string","enum":["inherited","loan","unknown"]},"reconstruction_level":{"type":["string","null"]},"reconstruction":{"type":["string","null"],"description":"WITHOUT the asterisk; render as *form."},"cognate_set_id":{"type":["string","null"]},"donor_language":{"type":["string","null"]},"donor_form":{"type":["string","null"]},"stratum":{"type":["string","null"]},"confidence":{"type":["string","null"],"enum":["certain","probable","possible",null]},"notes":{"type":["object","null"],"description":"A multilingual string: a map of locale code → text. Use `default` for the vernacular/headword writing system, and gloss-language codes (e.g. `en`, `es`) for glosses/translations. Most string inputs also accept a plain string, which is wrapped as `{ \"default\": \"…\" }`.","additionalProperties":{"type":"string"},"example":{"default":"mbwa","en":"dog"}},"citations":{"type":["array","null"],"items":{"$ref":"#/components/schemas/SourceCitation"}},"updated_at":{"type":"string","format":"date-time"}}}}}}