{
  "openapi": "3.0.3",
  "info": {
    "title": "LexAtlas API",
    "version": "0.2",
    "description": "Verifiable legal data for the CIS & Central Asia + international trade corpus, as an API.\n\nPrimitives: point-in-time law (as_of), citation verification, citecheck (finds citations that are not in the corpus or are no longer in force), hybrid cross-jurisdiction search, legislative changefeed, entry-into-force calendar, and multipolar sanctions screening (4 lists: US OFAC, UK, EU, Canada).\n\nEvery answer carries a trust envelope: source, official URL, licence, freshness, confidence tier. Norm texts are kept in the authentic language (no machine translation).\n\nAuth: X-API-Key header (or Authorization: Bearer). Keys are issued per client; each key has a per-minute rate limit.\n\nVERSIONING: external consumers MUST use the /v1 prefix — /v1/norm/{cite}, /v1/search, /v1/citecheck, /v1/act, /v1/norms. Inside /v1 we only ADD fields; a breaking change means /v2 living in parallel. The unversioned paths below are the same handlers kept for our internal tools and carry NO compatibility promise.\n\nTHREE CONTRACTS THAT HOLD ACROSS THE WHOLE SURFACE — write them once, they work on endpoints that do not exist yet:\n1. ERRORS. One envelope everywhere: {error (human text, may change), error_code (machine slug, never changes inside /v1), hint, http_status}. An error NEVER travels under a success code. The full code list is served by the API itself: GET /v1/errors.\n2. DOMAIN OUTCOMES ARE NOT ERRORS. {\"found\": false} is an answer to a valid question and travels under 200; so does outcome=not_in_corpus from /v1/answer_pack.\n3. PAGINATION. Every list endpoint carries the same `page` block: {limit, returned, truncated, next_cursor, hint}. Nothing is ever truncated silently. Where the row order is stable (the append-only changefeed) next_cursor is the position of the LAST ROW RETURNED — repeat the same request with cursor=next_cursor. Where it is not stable, next_cursor is null and the hint says so instead of pretending.",
    "contact": {
      "email": "business@creatora.app"
    }
  },
  "servers": [
    {
      "url": "https://api.lexatlas.creatora.app"
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "BearerKey": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error",
          "error_code"
        ],
        "description": "Uniform error envelope for the whole surface. Parse `error_code`, show `error`, act on `hint`.",
        "properties": {
          "error": {
            "type": "string",
            "description": "human text; may change"
          },
          "error_code": {
            "type": "string",
            "description": "machine slug; stable inside /v1 forever"
          },
          "hint": {
            "type": "string",
            "description": "what the client should do"
          },
          "http_status": {
            "type": "integer",
            "description": "duplicates the status code for proxies that lose it"
          }
        }
      },
      "Page": {
        "type": "object",
        "description": "Pagination block, identical on every list endpoint.",
        "properties": {
          "limit": {
            "type": "integer"
          },
          "returned": {
            "type": "integer"
          },
          "truncated": {
            "type": "boolean",
            "description": "proven by fetching limit+1 rows, not guessed from length"
          },
          "next_cursor": {
            "nullable": true,
            "description": "position of the LAST row returned; null where cursor paging is impossible"
          },
          "hint": {
            "type": "string"
          }
        }
      }
    }
  },
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "BearerKey": []
    }
  ],
  "paths": {
    "/v1/search": {
      "get": {
        "summary": "Hybrid search (FTS + vector, RRF) across jurisdictions and languages",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "query in any corpus language",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "jur",
            "in": "query",
            "required": false,
            "description": "jurisdiction filter (csv), e.g. ru,kz",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "k",
            "in": "query",
            "required": false,
            "description": "top-k (default 8); `limit` is accepted as a synonym",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "synonym of k",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "ranked hits with cite ids, snippets, licence and signal (vector/FTS); `jur_filter` (present only when `jur` was given) states whether we KNOW the named jurisdictions: {requested, unknown[], known, rows, note}. unknown is a LIST, so a mixed `ru,zz` names which one we lack. Empty results with known=true is an honest zero about the LAW; known=false is a fact about OUR coverage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/changes": {
      "get": {
        "summary": "Corpus changefeed since a date or a knowledge cursor (append-only, walkable)",
        "description": "Walk the feed: call once with since=<date|cursor>, then repeat with cursor=page.next_cursor until truncated=false. Rows come in FEED ORDER (cursor ascending) and next_cursor is the cursor of the LAST ROW RETURNED, so consecutive pages never overlap and never skip. A date in `since` is resolved to a starting position in the feed and echoed back as since_cursor — dates are a way to FIND a position, not a second paging mode. Each item carries its own `cursor`: stop anywhere, resume exactly there. `summary` counts the WHOLE tail behind the cursor, not just this page, so you can decide between paging through and dropping your cache. With as_of_knowledge=<cursor> the feed replays as it stood at that cursor.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "description": "YYYY-MM-DD or a knowledge cursor (integer)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "synonym of since; use page.next_cursor to walk the feed",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "jur",
            "in": "query",
            "required": false,
            "description": "jurisdiction filter (csv)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "max rows (default 100, cap 1000)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "as_of_knowledge",
            "in": "query",
            "required": false,
            "description": "replay the feed as of this knowledge cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{items[] with cursor, summary of the whole tail, page, knowledge}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/screen": {
      "get": {
        "summary": "Multipolar sanctions screening: name / alias / registered ID → measures across lists",
        "description": "Verbatim match against the 4 published official lists (US OFAC, UK, EU, Canada): exact name → alias → registered ID (Tax ID, IMO, registration number) → trigram-fuzzy with score. No generative step. A hit is not a determination; a no-hit is qualified by list coverage and freshness (screened_lists in the response). Canadian hits carry an `attribution` line (\"retrieved {date}\", Open Government Licence – Canada) and a `licence_url`.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "name, alias or registered ID",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "entity|person|vessel|aircraft (csv)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "jur",
            "in": "query",
            "required": false,
            "description": "imposing jurisdiction: us,uk,eu,ca (csv); outside keys: only the open lists",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "max matches (default 25, cap 200)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "matches with match_via/score + screened_lists coverage + notes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/pipeline": {
      "get": {
        "summary": "Legislative pipeline: bills/laws by stage across CIS+ (what is changing in law now)",
        "description": "Tracks the legislative lifecycle beyond the enacted corpus: introduced → readings → chambers → signed by head of state → published → in force. Each bill carries provenance (official publisher / parliament tracker) and, when matched, a link to the affected code in the corpus. Daily-refreshed.",
        "parameters": [
          {
            "name": "jur",
            "in": "query",
            "required": false,
            "description": "countries csv, e.g. ru,ge,kz",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "stage",
            "in": "query",
            "required": false,
            "description": "stages csv, e.g. published,signed_by_head_of_state,reading_3",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD — bills changed since this date",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "linked_only",
            "in": "query",
            "required": false,
            "description": "true → only bills matched to a corpus code",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "max bills (default 50)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "bills with stage, dates, provenance, corpus link + per-country coverage",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/citecheck": {
      "post": {
        "summary": "Check ALL legal references in a text against the corpus",
        "description": "Finds citations that are not in the corpus or are no longer in force; each finding carries the date the corpus last checked the source. Body: EITHER application/json {text, jur} OR text/plain. Malformed JSON or JSON without a non-empty `text` returns 400 — never a silent zero: a quiet {total:0} on a broken document would look exactly like 'no violations found'.",
        "parameters": [
          {
            "name": "default_jur",
            "in": "query",
            "required": false,
            "description": "jurisdiction assumed for bare citations",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string"
                  },
                  "jur": {
                    "type": "string"
                  }
                }
              }
            },
            "text/plain": {
              "schema": {
                "type": "string"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "per-citation verdicts: ok / repealed / not_found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "summary": "Service health + endpoint list (no auth)",
        "security": [],
        "responses": {
          "200": {
            "description": "service status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/limits": {
      "get": {
        "summary": "Rate limit and remaining quota for the calling key",
        "responses": {
          "200": {
            "description": "{rate_per_min, remaining_this_minute, org}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/act": {
      "get": {
        "summary": "Resolve an act by its requisites (date + number) → internal code",
        "description": "Lawyers cite acts by requisites, not by code: 'Plenum ruling of 28.06.2012 No. 17'. Key is strict: type + number + date. A bare number returns ambiguous:true with the list — an act number alone is NOT an identifier (843 documents share number '253' in the RF gazette).",
        "parameters": [
          {
            "name": "jur",
            "in": "query",
            "required": true,
            "description": "jurisdiction, e.g. ru",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date",
            "in": "query",
            "required": false,
            "description": "act date YYYY-MM-DD",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "no",
            "in": "query",
            "required": false,
            "description": "act number",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "description": "act kind filter, e.g. court_practice",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "matching acts with codes, titles and where they were found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/act/toc": {
      "get": {
        "summary": "Table of contents of an act: its structure as we hold it",
        "description": "Sections and articles of one act, in source order, with the article count we actually indexed — so a gap in the corpus is visible as a gap, not as a shorter list.",
        "parameters": [
          {
            "name": "jur",
            "in": "query",
            "required": true,
            "description": "jurisdiction, e.g. ru",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code",
            "in": "query",
            "required": true,
            "description": "act code, e.g. gk_rf_1",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{act, units: [{no, title, kind}], articles, articles_indexed}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/answer_pack": {
      "post": {
        "summary": "Everything an answer needs — fragments, mandatory citations, deterministic outcome. No generation.",
        "description": "We do NOT write the answer: your model writes it, on your key, under your editorial responsibility. We return what you cannot compute yourself — the norms found verbatim, the citations the answer MUST rest on, and the OUTCOME, decided against the corpus denominator:\n· supported — norms found; build the answer on must_cite;\n· not_in_corpus — nothing relevant AND the jurisdiction's denominator is not closed: honest 'we do not know';\n· no_requirement_found — nothing relevant AND the denominator IS closed: 'there is no such requirement'. This third outcome is a privilege, not a convenience: a single caveat about completeness disqualifies it (see `denominator`).\n\nRelevance is gated deterministically, not by cosine distance: a fragment is `strong` only on LEXICAL confirmation — the question's own word stems occur in the norm (overlap ≥ 0.34) or the dictionary channel (FTS) found it. Vector-only hits are returned as `weak` and never enter must_cite: a nearest neighbour always exists, including when the corpus holds nothing on the topic. Agreement between our two vector channels (article and chunk) is NOT counted as evidence — it is one embedding model applied twice.\n\nRepealed norms stay in `fragments` (a lawyer must SEE that the article on the topic was repealed) but are excluded from must_cite via eligible_for_citation=false.\n\nBody: {question, jur (required), k (default 6, cap 20), as_of, lang}. Then run the finished answer through POST /v1/citecheck before showing it to a user: that keeps the 'no invented article' guarantee closed on the corpus.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "question",
                  "jur"
                ],
                "properties": {
                  "question": {
                    "type": "string"
                  },
                  "jur": {
                    "type": "string"
                  },
                  "k": {
                    "type": "integer"
                  },
                  "as_of": {
                    "type": "string"
                  },
                  "lang": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{outcome, outcome_basis, must_cite[], fragments[] with verbatim text + licence + status + relevance grade, answer_contract, denominator, knowledge}. A fragment may carry relevance.citation_blocked — the corpus WITHDRAWS its own support for that norm: show it, do not rely on it. Closed enumeration, listed below; an unknown value must be read as a refusal, never as 'nothing blocked'. Values: `confirmation_scattered` — the words that confirmed the norm stand farther apart than the span threshold: in a long act a chance co-occurrence of words does not mean the norm is on point",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/errors": {
      "get": {
        "summary": "The error contract itself, served by the API (no auth)",
        "description": "Machine-readable list of every error_code, its HTTP status and meaning, plus the domain outcomes that are NOT errors. A documentation page goes stale silently; this endpoint cannot.",
        "security": [],
        "responses": {
          "200": {
            "description": "{errors[], domain_outcomes[], envelope, invariant}",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/calendar/upcoming": {
      "get": {
        "summary": "Upcoming entries into force (predictive fact)",
        "parameters": [
          {
            "name": "jur",
            "in": "query",
            "required": false,
            "description": "jurisdiction filter (csv)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "months",
            "in": "query",
            "required": false,
            "description": "horizon (default 24)",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "upcoming provisions with dates and sources",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/norm/{cite}": {
      "get": {
        "summary": "Article text + act + status + freshness + provenance; as_of=YYYY-MM-DD → point-in-time",
        "description": "Returns the article with the trust envelope. With as_of, the text is served from the amendment reconstruction (point-in-time, 'git for law') with the governing act attributed; without it — the current consolidated text.",
        "parameters": [
          {
            "name": "cite",
            "in": "path",
            "required": true,
            "description": "Normalized citation id: jur:code:article_no[:lang], e.g. ru:gk_rf_1:196 or kz:gk_kz_1:178. One scheme across all jurisdictions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "ru|en trust-envelope language",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "as_of",
            "in": "query",
            "required": false,
            "description": "YYYY-MM-DD — reconstruct the text as of this date",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "context",
            "in": "query",
            "required": false,
            "description": "the request text (a claim, a question). When the cite names an act by number without a year and several acts carry that number (not_found_reason=ambiguous_number, e.g. ru:fz_244:5), the act whose meaning matches this text is chosen: resolved_reason=act_number_context, context_scores, same_number_acts. No clear winner — the candidates as before",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "article + trust envelope (+ as_of_state, governing_act when as_of)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/verify/{cite}": {
      "get": {
        "summary": "Verify a citation: exists? in force? verbatim quotable? how fresh?",
        "parameters": [
          {
            "name": "cite",
            "in": "path",
            "required": true,
            "description": "Normalized citation id: jur:code:article_no[:lang], e.g. ru:gk_rf_1:196 or kz:gk_kz_1:178. One scheme across all jurisdictions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "ru|en",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "verdict + provenance + amendment summary + confidence tier",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/history/{cite}": {
      "get": {
        "summary": "Amendment history of an article (when and by which law it changed)",
        "parameters": [
          {
            "name": "cite",
            "in": "path",
            "required": true,
            "description": "Normalized citation id: jur:code:article_no[:lang], e.g. ru:gk_rf_1:196 or kz:gk_kz_1:178. One scheme across all jurisdictions.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "ru|en",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "list of amendments with dates, acts and reconstruction status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "description": "bad request — see error_code (missing_parameter, invalid_parameter, invalid_body, invalid_cursor, empty_batch, batch_too_large, unsupported)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "missing/invalid API key (when auth is enabled) — error_code=unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "body too large — error_code=payload_too_large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate limit exceeded (per-key, per-minute window) — error_code=rate_limited",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}