# Ordinis API — Quick Start

Practical guide to НСИ-ordinis REST API. Тут не справочник всех endpoint'ов
(для этого есть OpenAPI / Swagger UI), а минимально-достаточный путь от
«первый запрос» до «подписался на webhook».

**Версия:** v2.37.x (`/api/v1`)
**Swagger UI:** `https://ordinis.k8s.265.nstart.cloud/swagger-ui/index.html`
(staging — на prod env-flagged)

---

## 0. Принципы

- REST + JSON. UTF-8. Все datetime в ISO-8601 UTC (`Z` suffix).
- Базовый префикс — `/api/v1`. Версионирование URL (breaking changes → `/v2`).
- Auth — Keycloak OIDC, Bearer-токен в `Authorization`. Anon разрешён для
  `PUBLIC` scope.
- Pagination — на records list **не поддерживается**, возвращается весь массив. Для больших dicts фильтруй на клиенте или через `?at=` time-slice.
- Ошибки — `application/json` с `{status, code, message, traceId, timestamp}`.

## 1. Base URL и проверка живости

```bash
# Production
ORDINIS=https://ordinis.nstart.cloud   # prod (public LE)
# ORDINIS=https://ordinis.k8s.265.nstart.cloud   # staging

# Staging
# ORDINIS=https://ordinis.k8s.265.nstart.cloud

curl "$ORDINIS/api/v1/version"
```

Ответ:

```json
{
  "version": "0.1.0-SNAPSHOT",
  "commit": "2ca8f053",
  "branch": "main",
  "tag": "v2.37.0",
  "builtAt": "2026-05-17T08:18:07.437Z"
}
```

## 2. Аутентификация

Большинство read-эндпоинтов работают без авторизации в `PUBLIC` scope.
Mutations, INTERNAL/RESTRICTED data, workflow и `/me/*` требуют JWT.

### 2.1 Получение токена через Keycloak (password grant)

```bash
TOKEN=$(curl -s -X POST \
  "https://altum.nstart.cloud/auth/realms/altum/protocol/openid-connect/token" \
  -d "client_id=ordinis" \
  -d "grant_type=password" \
  -d "username=user@example.com" \
  -d "password=...." \
  -d "scope=openid profile email" \
  | jq -r .access_token)
```

> Password grant — для CLI/CI/тестов. В production — OIDC Authorization Code
> с PKCE через ваш BFF.

### 2.2 Использование токена

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/me/notifications/settings"
```

### 2.3 Скоупы данных

- `PUBLIC` — открыто всем, включая anon
- `INTERNAL` — только для авторизованных с ролью
- `RESTRICTED` — узкий круг, как правило admin

Sidebar и search автоматически фильтруют по scope текущей сессии.

## 3. Справочники (dictionaries)

### 3.1 Список всех справочников

```bash
curl "$ORDINIS/api/v1/dictionaries"
```

```json
[
  {
    "id": "58ba2127-...",
    "name": "spacecraft",
    "displayName": "Космические аппараты",
    "description": "Каталог КА (космических аппаратов) ДЗЗ-миссий",
    "scope": "PUBLIC",
    "schemaVersion": "1.0.0",
    "bundle": "cuod",
    "supportedLocales": ["ru-RU", "en-US"],
    "defaultLocale": "ru-RU",
    "createdAt": "2026-05-07T21:20:51.623195Z",
    "updatedAt": "2026-05-07T21:20:51.623195Z"
  },
  ...
]
```

### 3.2 Детали одного справочника (со схемой)

```bash
curl "$ORDINIS/api/v1/dictionaries/spacecraft"
```

`schemaJson` — полный JSON Schema draft-07 с x-расширениями:

```json
{
  "name": "spacecraft",
  "scope": "PUBLIC",
  "schemaJson": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "https://ordinis.nstart.cloud/schemas/cuod/spacecraft/1.0.0",
    "type": "object",
    "required": ["designator", "name", "satellite_type_code", "status"],
    "properties": {
      "name": {
        "type": "object",
        "required": ["ru-RU"],
        "x-localized": true,
        "patternProperties": { "^[a-z]{2}-[A-Z]{2}$": { "type": "string" } }
      },
      "satellite_type_code": {
        "type": "string",
        "x-references": "satellite_type.code"
      }
    }
  }
}
```

### 3.3 Граф зависимостей (FK)

```bash
curl "$ORDINIS/api/v1/dictionaries/graph/outgoing"
```

```json
{
  "outgoing": {
    "spacecraft": { "fkCount": 2, "targets": ["satellite_type", "spacecraft_status"] },
    "antenna":    { "fkCount": 1, "targets": ["ground_station"] }
  }
}
```

## 4. Записи (records)

> **Внимание к URL:** записи живут под `/api/v1/{dictName}/records`
> (а не `/api/v1/dictionaries/{name}/records` — это путь для drafts schema'ы).

### 4.1 Список записей

```bash
curl "$ORDINIS/api/v1/spacecraft/records?as_scope=PUBLIC"
```

`as_scope` обязателен (CSV: `PUBLIC,INTERNAL,RESTRICTED`).

```json
[
  {
    "id": "a211eb15-...",
    "businessKey": "1999-068A",
    "data": {
      "name": "Terra (EOS AM-1)",
      "orbit": { "type": "SSO", "inclination_deg": 98.2 },
      "status": "OPERATIONAL",
      "country": "US",
      "operator": "НАСА",
      "satellite_type_code": "OPT_MIDRES_MS"
    },
    "validFrom": "2026-05-08T00:00:00Z",
    "validTo": null,
    "dataScope": "PUBLIC"
  }
]
```

### 4.2 Одна запись по business key

```bash
curl "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC"
```

### 4.3 Фильтры

```bash
# Locale — для x-localized полей (name, mission, operator etc).
# ⚠️ Сейчас работает ТОЛЬКО через Accept-Language header. ?locale= query
# param backend игнорирует — TODO в roadmap.
curl -H "Accept-Language: en-US" \
  "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC"
# default ru-RU:    "mission": "Глобальный мониторинг суши и атмосферы"
# с Accept-Language en-US:  "mission": "Global land and atmosphere monitoring"

# Time-travel: состояние на конкретный момент (validFrom <= at < validTo)
curl "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC&at=2026-01-01T00:00:00Z"

# По scope (если есть права на INTERNAL/RESTRICTED)
curl -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/spacecraft/records?as_scope=PUBLIC,INTERNAL"

# Geo (для geo-dicts с PostGIS support)
curl "$ORDINIS/api/v1/ground_station/records?as_scope=PUBLIC&bbox=30,55,40,60"
```

⚠️ **НЕ работает на records list endpoint**: `?limit=`, `?offset=` (pagination отсутствует), `?status=...&country=...` (нет ad-hoc filtering по schema полям). Фильтруй на клиенте.

### 4.4 Сводка по запланированным записям

```bash
curl "$ORDINIS/api/v1/spacecraft/records/scheduled-summary?as_scope=PUBLIC"
```

```json
{
  "count": 0,
  "nearestValidFrom": null,
  "upcomingValidFroms": [],
  "scheduledBusinessKeys": []
}
```

## 5. Smart Search

Поиск по содержимому всех справочников. Использует trigram-индекс.

```bash
curl "$ORDINIS/api/v1/search?q=Sentinel&limit=10"
```

```json
{
  "query": "Sentinel",
  "total": 12,
  "groups": [
    {
      "dictName": "mission",
      "dictDisplayName": "Миссии / программы",
      "count": 2,
      "items": [
        { "businessKey": "SENTINEL_1", "dataScope": "PUBLIC", "createdAt": "..." },
        { "businessKey": "SENTINEL_2", "dataScope": "PUBLIC", "createdAt": "..." }
      ]
    },
    {
      "dictName": "instrument",
      "dictDisplayName": "Инструменты съёмки",
      "count": 2,
      "items": [ ... ]
    }
  ]
}
```

Минимальная длина запроса — **3 символа**. Search ограничен текущим scope сессии.

## 6. Bitemporal queries (TimeTravel)

### 6.1 История изменений записи

```bash
curl "$ORDINIS/api/v1/spacecraft/records/1999-068A/history"
```

Возвращает все версии (по `valid_from`) и transaction history (`system_time`).

### 6.2 Snapshot справочника на дату

```bash
curl "$ORDINIS/api/v1/dictionaries/spacecraft/snapshots?from=2026-01-01T00:00:00Z&to=2026-05-01T00:00:00Z&granularity=day"
```

Точная реконструкция справочника на момент `asOf`. Все записи, которые
**действовали** (`valid_from ≤ asOf < valid_to`) и были **известны системе**
(`system_time ≤ now()`).

### 6.3 Changelog схемы (структурные изменения)

```bash
curl "$ORDINIS/api/v1/dictionaries/spacecraft/changelog"
```

```json
{
  "dictionary": "spacecraft",
  "currentVersion": "1.0.0",
  "entries": []
}
```

Диff между двумя версиями схемы:

```bash
curl "$ORDINIS/api/v1/dictionaries/spacecraft/changelog/{entryId}/diff"
```

## 7. Шаблоны схем

```bash
curl "$ORDINIS/api/v1/dictionaries/templates"
```

```json
[
  {
    "id": "antenna",
    "name": "Антенна",
    "description": "Скелет: код, имя, диаметр, FK на наземную станцию.",
    "icon": "WifiHighIcon",
    "tags": ["space", "hardware", "cuod"]
  },
  { "id": "blank", "name": "Пустой каркас", ... },
  { "id": "spacecraft", "name": "Космический аппарат", ... }
]
```

Детали шаблона:

```bash
curl "$ORDINIS/api/v1/dictionaries/templates/spacecraft"
```

Вернёт `{ id, name, description, schemaJson }` со sample JSON Schema.

## 8. AI: suggest field (требует auth)

Проверка фичи:

```bash
curl "$ORDINIS/api/v1/ai/info"
# { "enabled": true }
```

Если `enabled: false` — endpoint `/suggest-field` отдаст 404.

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$ORDINIS/api/v1/ai/suggest-field" \
  -d '{"prompt": "контактный email оператора"}'
```

```json
{
  "fieldName": "operator_contact_email",
  "schema": {
    "type": "string",
    "format": "email",
    "description": "Контактный email оператора",
    "x-references": "operator.code"
  }
}
```

**Лимиты:** 30 запросов/мин на IP. Circuit breaker открывается на 5 минут
после 10 ошибок за 60 секунд (`503 circuit_open`).

## 9. Workflow согласований (требует auth)

### 9.1 Создать draft записи

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$ORDINIS/api/v1/dictionaries/spacecraft/records/drafts" \
  -d '{
    "operation": "CREATE",
    "businessKey": "2026-099X",
    "data": { "name": "Новый-1", "status": "PLANNED", ... },
    "validFrom": "2026-06-01T00:00:00Z"
  }'
```

Если `approvalRequired=true` у справочника — draft уходит ревьюеру.
Иначе — record создаётся напрямую (если у тебя есть права).

### 9.2 Мои drafts

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/drafts/me"
```

### 9.3 Очередь ревьюера

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/admin/reviews/pending"
```

### 9.4 Approve / reject

```bash
# Approve
curl -X POST -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/drafts/{id}/approve" \
  -d '{ "comment": "ok, merging" }'

# Reject
curl -X POST -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/drafts/{id}/reject" \
  -d '{ "comment": "не хватает источника данных" }'
```

## 10. Webhooks (admin)

### 10.1 Создать подписку

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$ORDINIS/api/v1/admin/webhooks/subscriptions" \
  -d '{
    "name": "my-consumer",
    "url": "https://my-app.example.com/ordinis-hook",
    "eventTypes": ["RecordCreated", "RecordUpdated", "SchemaDraftPublished"],
    "scopeFilter": ["PUBLIC", "INTERNAL"],
    "dictionaries": null,
    "textWrap": false
  }'
```

Ответ содержит **`secret`** — HMAC-SHA256 ключ для верификации подписи.
Покажется один раз, сохрани.

### 10.2 Структура payload (события)

```json
{
  "eventId": "evt_01HXY...",
  "eventType": "RecordCreated",
  "occurredAt": "2026-05-17T13:00:00Z",
  "dictionary": "spacecraft",
  "businessKey": "2026-099X",
  "actor": "user-uuid-...",
  "scope": "PUBLIC",
  "after": { ... }
}
```

Заголовки:
- `X-Ordinis-Event: RecordCreated`
- `X-Ordinis-Signature: sha256=<hex>` (HMAC-SHA256 от body)
- `X-Ordinis-Delivery: del_01HXY...`
- `User-Agent: ordinis-alerts`

### 10.3 Верификация подписи (Node.js)

```js
import crypto from 'crypto'

function verify(body, signatureHeader, secret) {
  const [algo, hex] = signatureHeader.split('=')
  if (algo !== 'sha256') return false
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(hex))
}
```

### 10.4 Retry + DLQ

Доставка ретраится с backoff (1m, 5m, 30m, 2h, 12h). После 5 неудач —
в DLQ. Ручной retry:

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  "$ORDINIS/api/v1/admin/webhooks/deliveries/{id}/retry"
```

## 11. Errors

Все ошибки — JSON одинаковой формы:

```json
{
  "status": 422,
  "code": "validation_failed",
  "message": "name.ru-RU is required",
  "details": { "field": "data.name.ru-RU" },
  "traceId": "abc123...",
  "timestamp": "2026-05-17T13:00:00Z"
}
```

| HTTP | code | Когда |
|---|---|---|
| 400 | `bad_request` | Невалидный JSON, отсутствует обязательный field |
| 401 | `unauthorized` | Нет или просрочен токен |
| 403 | `forbidden` | Недостаточно прав / scope |
| 404 | `not_found` | Нет такого справочника / записи / endpoint |
| 409 | `conflict` | Concurrent update / unique constraint |
| 422 | `validation_failed` | Schema validation fail |
| 429 | `rate_limited` | Превышен лимит запросов |
| 500 | `internal_error` | Серверная ошибка (см. `traceId`) |
| 502 | `llm_error` | AI suggest — LLM недоступен |
| 503 | `circuit_open` | AI suggest — circuit breaker открыт |

## 12. Готовые сниппеты

### curl + jq (CI/CD)

```bash
# Все записи операторов в PUBLIC scope; фильтрация на клиенте
curl -s "$ORDINIS/api/v1/operator/records?as_scope=PUBLIC" \
  | jq '.[] | select(.data.status == "ACTIVE") | {key: .businessKey, name: .data.name}'
```

### TypeScript / Fetch

```ts
const ORDINIS = 'https://ordinis.nstart.cloud'

type SpacecraftRecord = {
  businessKey: string
  data: { name: string; status: string; orbit: { type: string } }
  validFrom: string
  dataScope: 'PUBLIC' | 'INTERNAL' | 'RESTRICTED'
}

async function getSpacecraft(): Promise<SpacecraftRecord[]> {
  const r = await fetch(
    `${ORDINIS}/api/v1/spacecraft/records?as_scope=PUBLIC`,
    { headers: { Accept: 'application/json', 'Accept-Language': 'en-US' } },
  )
  if (!r.ok) throw new Error(`Ordinis ${r.status}`)
  return r.json()
}
```

### Python (requests)

```python
import requests

ORDINIS = "https://ordinis.nstart.cloud"

def smart_search(query: str, limit: int = 10) -> dict:
    r = requests.get(f"{ORDINIS}/api/v1/search",
                     params={"q": query, "limit": limit},
                     timeout=10)
    r.raise_for_status()
    return r.json()

result = smart_search("Sentinel")
for group in result["groups"]:
    print(f'{group["dictDisplayName"]}: {group["count"]} hits')
```

### Java (HttpClient)

```java
HttpClient http = HttpClient.newHttpClient();

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create(ORDINIS + "/api/v1/dictionaries/spacecraft"))
    .header("Accept", "application/json")
    .GET()
    .build();

HttpResponse<String> resp = http.send(req, BodyHandlers.ofString());
DictionaryDetail detail = mapper.readValue(resp.body(), DictionaryDetail.class);
```

## 13. Дальше

- **Приложение A: Cheat Sheet** — одностраничник со всеми endpoints, ниже в этом документе
- **OpenAPI:** `$ORDINIS/v3/api-docs` (staging) / Swagger UI выше
- **Postman collection:** `~/Desktop/Ordinis-Postman/` (27 рабочих запросов + 10-шаговый demo flow)
- **Bundle spec (для интеграторов):** [`bundle-format-spec.md`](./bundle-format-spec.md)
- **CHANGELOG:** [`docs/changelog.md`](../changelog.md) — что меняется по версиям
- **Webhooks deep-dive:** в admin UI → `/webhooks` → Detail page
  показывает histogram доставок, DLQ, signature test

---

# Приложение A · Cheat Sheet

> Дублирует [`api-cheatsheet.md`](./api-cheatsheet.md) — одностраничный quick-reference со всеми endpoints, query-params, error codes и реальными примерами для copy-paste. Дублирование осознанное: интегратор открывает quickstart один раз, далее живёт в приложении.


Одностраничный референс. Полный гайд — `api-quickstart.md`.

**Base prod:** `https://ordinis.nstart.cloud/api/v1` (public LE-cert)
**Base staging:** `https://ordinis.k8s.265.nstart.cloud/api/v1` (Swagger UI здесь)
**Version:** `v2.37.x` · **OpenAPI:** `/v3/api-docs` (только staging — на prod Swagger выключен)

## Auth

```bash
# client_credentials (для service-to-service интеграторов)
TOKEN=$(curl -s -X POST https://altum.nstart.cloud/auth/realms/altum/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=$KC_CLIENT_ID -d client_secret=$KC_CLIENT_SECRET | jq -r .access_token)

curl -H "Authorization: Bearer $TOKEN" $ORDINIS/api/v1/spacecraft/records?as_scope=PUBLIC,INTERNAL
```

Scopes: `PUBLIC` (anon ok) · `INTERNAL` (auth) · `RESTRICTED` (admin).

## Read (no auth для PUBLIC)

| Verb | Path | Что |
|---|---|---|
| `GET` | `/version` | Версия + commit + builtAt |
| `GET` | `/dictionaries` | Список всех справочников |
| `GET` | `/dictionaries/{name}` | Детали + JSON Schema |
| `GET` | `/dictionaries/templates` | 6 шаблонов схем |
| `GET` | `/dictionaries/templates/{id}` | Один шаблон со schemaJson |
| `GET` | `/dictionaries/graph/outgoing` | FK-граф между справочниками |
| `GET` | `/dictionaries/{name}/dependents` | Кто ссылается на этот dict |
| `GET` | `/dictionaries/{name}/changelog` | Список изменений схемы |
| `GET` | `/dictionaries/{name}/changelog/{id}/diff` | Diff между версиями |
| `GET` | `/dictionaries/{name}/snapshots?from=&to=&granularity=` | Bucket'ы изменений для TimeTravel slider (granularity: hour/day/week) |
| `GET` | `/{name}/records?as_scope=PUBLIC` | Список записей (⚠️ **БЕЗ** `/dictionaries/` префикса) |
| `GET` | `/{name}/records/{businessKey}?as_scope=PUBLIC` | Одна запись |
| `GET` | `/dictionaries/{name}/records/{businessKey}/history?as_scope=PUBLIC` | История версий записи (Envers) — **с** `/dictionaries/` |
| `GET` | `/dictionaries/{name}/records/{businessKey}/cascade-preview?as_scope=PUBLIC` | Preview каскада — **с** `/dictionaries/` |
| `GET` | `/{name}/records/scheduled-summary?as_scope=PUBLIC` | Будущие версии (count + nearestValidFrom) |
| `GET` | `/search?q=` | Поиск по справочникам (records не индексируются) |
| `GET` | `/ai/info` | `{enabled: bool}` AI feature flag |

> ⚠️ **Корень частой путаницы**: consumer endpoints (`/records/...`) идут БЕЗ `/dictionaries/` префикса. Admin-style audit endpoints (history, cascade-preview, changelog, drafts, snapshots) — С `/dictionaries/`. См. [api-quickstart.md § Topology](api-quickstart.md).

### Query params (records)

```
?as_scope=PUBLIC,INTERNAL    (required) CSV scope filter
?at=2026-03-15T00:00:00Z     bitemporal: запись на конкретный момент (validFrom <= at < validTo)
Accept-Language: en-US       (header) локаль — единственный рабочий путь
                             ⚠️ ?locale= query param backend сейчас игнорирует
                             ⚠️ Работает ТОЛЬКО на records endpoints.
                                Schema endpoint (`/dictionaries/{name}`) всегда
                                возвращает schemaJson на defaultLocale.
?bbox=lon1,lat1,lon2,lat2    geo: bounding box (для geo-dicts)
?polygon=<GeoJSON>           geo: polygon-фильтр
```

**Что НЕ работает (на read-api endpoint):**
- `?limit=`, `?offset=` — pagination отсутствует, возвращается весь массив
- `?status=ACTIVE&country=RU` — нет ad-hoc filtering по schema полям; фильтруй на клиенте

## Write (требует auth)

⚠️ Все write-endpoints — на **writer** сервисе, с `/dictionaries/` префиксом.

| Verb | Path | Что |
|---|---|---|
| `POST` | `/dictionaries/{name}/records` | Создать запись (если approval не нужен) |
| `PUT` | `/dictionaries/{name}/records/{businessKey}` | Обновить |
| `DELETE` | `/dictionaries/{name}/records/{businessKey}` | Soft-delete (tombstone) |
| `POST` | `/dictionaries/{name}/records/bulk-close` | Массовое закрытие |
| `POST` | `/dictionaries/{name}/records/{businessKey}/cascade-close` | Закрыть с зависимыми |
| `POST` | `/dictionaries/{name}/records/export-selected.csv` | Bulk export (выборка по ids в body) |
| `PATCH` | `/dictionaries/{name}/schema` | Изменить схему inline (non-breaking) |
| `PUT` | `/dictionaries/{name}` | Полностью пересоздать (breaking) |

## Workflow (drafts + approvals)

| Verb | Path | Что |
|---|---|---|
| `POST` | `/dictionaries/{name}/records/drafts` | Создать record draft |
| `POST` | `/dictionaries/{name}/drafts` | Создать schema draft |
| `GET` | `/dictionaries/{name}/drafts` | Список schema drafts по dict |
| `GET` | `/dictionaries/{name}/drafts/active` | Активный draft (200+null если нет) |
| `GET` | `/dictionaries/{name}/drafts/{id}` | Один draft |
| `DELETE` | `/dictionaries/{name}/drafts/{id}` | Withdraw |
| `POST` | `/dictionaries/{name}/drafts/{id}/review` | Назначить ревью |
| `POST` | `/dictionaries/{name}/drafts/{id}/decision` | Approve/reject schema draft |
| `POST` | `/dictionaries/{name}/drafts/{id}/publish` | Опубликовать после approve |
| `GET` | `/drafts/me` | Мои record-drafts (status filter) |
| `GET` | `/drafts/{id}` | Один record draft |
| `DELETE` | `/drafts/{id}` | Withdraw record draft |
| `POST` | `/drafts/{id}/approve` | Одобрить (body: `{comment}`) |
| `POST` | `/drafts/{id}/reject` | Отклонить (body: `{comment}`) |
| `GET` | `/admin/reviews/pending` | Очередь ревьюера (records) |
| `GET` | `/admin/schema-reviews/pending` | Очередь ревьюера (schemas) |
| `GET` | `/admin/schema-drafts/me` | Мои schema drafts |

## AI

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  $ORDINIS/api/v1/ai/suggest-field \
  -d '{"prompt":"контактный email оператора"}'
```

Limits: **30 req/min** на IP, circuit breaker **10 fails / 60s → open 5min** (`503 circuit_open`).

## Notifications (per-user, auth)

| Verb | Path | Что |
|---|---|---|
| `GET` | `/me/notifications` | Bell-bar feed |
| `POST` | `/me/notifications/{id}/read` | Mark as read |
| `POST` | `/me/notifications/read-all` | Mark all as read |
| `GET` | `/me/notifications/preferences` | Per-event matrix |
| `PUT` | `/me/notifications/preferences` | Сохранить матрицу |
| `GET` | `/me/notifications/settings` | Quiet hours config |
| `PUT` | `/me/notifications/settings` | Сохранить quiet hours |

## Webhooks (admin)

| Verb | Path | Что |
|---|---|---|
| `GET` | `/admin/webhooks/subscriptions` | Список подписок |
| `POST` | `/admin/webhooks/subscriptions` | Создать (`secret` в ответе, **один раз**) |
| `GET` | `/admin/webhooks/subscriptions/{id}` | Детали |
| `PUT` | `/admin/webhooks/subscriptions/{id}` | Обновить |
| `DELETE` | `/admin/webhooks/subscriptions/{id}` | Удалить |
| `POST` | `/admin/webhooks/subscriptions/{id}/rotate-secret` | Перевыпуск ключа |
| `POST` | `/admin/webhooks/subscriptions/{id}/test` | Test delivery |
| `GET` | `/admin/webhooks/subscriptions/{id}/stats` | Histogram (24h/7d) |
| `GET` | `/admin/webhooks/subscriptions/{id}/deliveries` | Recent deliveries |
| `GET` | `/admin/webhooks/deliveries/dlq` | DLQ list |
| `POST` | `/admin/webhooks/deliveries/{id}/retry` | Manual retry |

### Headers delivered

```
X-Ordinis-Event: RecordCreated
X-Ordinis-Signature: sha256=<hex>     HMAC-SHA256(body, secret)
X-Ordinis-Delivery: del_01HXY...
User-Agent: ordinis-alerts
```

### Event types

`RecordCreated · RecordUpdated · RecordClosed · RecordRestored · BulkClosed · SchemaDraftSubmitted · SchemaDraftApproved · SchemaDraftRejected · SchemaDraftPublished · RecordDraftSubmitted · RecordDraftApproved`

## Audit (admin)

| Verb | Path | Что |
|---|---|---|
| `GET` | `/admin/audit?dictionary=&action=&user=&dateFrom=&dateTo=` | Фильтрованный аудит |
| `GET` | `/admin/audit/export.csv` | Полный экспорт |
| `GET` | `/admin/audit/export-selected.csv` | Selected ids экспорт |

Action types (13): `DICTIONARY_CREATED · SCHEMA_UPDATED · SCHEMA_PUBLISHED · RECORD_CREATED · RECORD_UPDATED · RECORD_CLOSED · RECORD_RESTORED · DRAFT_CREATED · DRAFT_SUBMITTED · DRAFT_APPROVED · DRAFT_REJECTED · DRAFT_WITHDRAWN · REVIEWER_ASSIGNED`

## Outbox (admin)

| Verb | Path | Что |
|---|---|---|
| `GET` | `/admin/outbox/stats` | Pending/DLQ counters |
| `GET` | `/admin/outbox/dlq` | DLQ events list |

## Errors — единая форма

```json
{ "status": 422, "code": "validation_failed", "message": "...",
  "details": {}, "traceId": "...", "timestamp": "..." }
```

| HTTP | code | |
|---|---|---|
| 400 | `bad_request` | Невалидный body |
| 401 | `unauthorized` | Нет/просрочен токен |
| 403 | `forbidden` | Scope/role не пускает |
| 404 | `not_found` | Нет такого ресурса |
| 409 | `conflict` | Concurrent update / unique violation |
| 422 | `validation_failed` | Schema validation fail |
| 429 | `rate_limited` | Лимит запросов |
| 500 | `internal_error` | См. traceId |
| 502 | `llm_error` | AI: LLM upstream |
| 503 | `circuit_open` | AI: circuit breaker |

## Реальные примеры (staging 265)

### FK-зависимость: кто использует справочник

```bash
curl -s "$ORDINIS/api/v1/dictionaries/satellite_type/dependents" | jq
# [
#   {
#     "sourceDict": "spacecraft",
#     "sourceDisplayName": "Космические аппараты",
#     "sourceField": "satellite_type_code",
#     "targetField": "code",
#     "onClose": "BLOCK",
#     "activeRecordsInSourceDict": 14
#   }
# ]
```

**Семантика**: `spacecraft.satellite_type_code` ссылается на `satellite_type.code`. На staging 14 КА используют этот справочник, и `onClose: BLOCK` означает что нельзя удалить тип КА пока на него ссылаются записи (без cascade-close).

Другие живые FK на staging:
- `ground_station` ← `antenna.ground_station` (4 records, BLOCK)
- `spacecraft_status` ← `spacecraft.status`
- `country` ← `spacecraft.country`

### Локализация записи (Accept-Language)

```bash
# default ru-RU
curl -s "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC" | jq '.data | {mission, operator}'
# {"mission": "Глобальный мониторинг суши и атмосферы", "operator": "НАСА"}

# en-US
curl -sH "Accept-Language: en-US" \
  "$ORDINIS/api/v1/spacecraft/records/1999-068A?as_scope=PUBLIC" | jq '.data | {mission, operator}'
# {"mission": "Global land and atmosphere monitoring", "operator": "NASA"}
```

⚠️ Только Accept-Language header. `?locale=` query param backend сейчас игнорирует. Schema endpoint (`/dictionaries/{name}`) **не локализуется** вообще — всегда возвращает на defaultLocale.

### Time-travel: запись на момент в прошлом

```bash
# Запись на 14.05.2026 12:00 UTC (точно после её validFrom = 11:34)
curl -s "$ORDINIS/api/v1/test1/records/Dima_record_test_1?as_scope=PUBLIC&at=2026-05-14T12:00:00Z" \
  | jq '{businessKey, validFrom, validTo, data}'
# 200 OK — запись действительная на указанный момент

# До validFrom → 404 record_not_active (это ожидаемо)
curl -s "$ORDINIS/api/v1/test1/records/Dima_record_test_1?as_scope=PUBLIC&at=2026-05-01T00:00:00Z" | jq '.code'
# "record_not_active"
```

### History — все ревизии записи

```bash
curl -s "$ORDINIS/api/v1/dictionaries/spacecraft/records/1999-068A/history?as_scope=PUBLIC" | jq '.[0] | {revisionNumber, revisionType, updatedAt}'
```

### Schema time-travel (changelog diff)

```bash
# Список ревизий
curl -s "$ORDINIS/api/v1/dictionaries/test1/changelog" | jq '.currentVersion, (.entries | length)'

# Diff конкретной ревизии (before/after JSON-Schema)
curl -s "$ORDINIS/api/v1/dictionaries/test1/changelog/22/diff" | jq '{eventType, occurredAt, before: .before.properties | keys, after: .after.properties | keys}'
```

## 60-second smoke

```bash
ORDINIS=https://ordinis.nstart.cloud   # prod (public LE)
# или: ORDINIS=https://ordinis.k8s.265.nstart.cloud   # staging

curl -s $ORDINIS/api/v1/version | jq                                          # alive
curl -s $ORDINIS/api/v1/dictionaries | jq 'length'                            # 37 на prod / 39 на staging
curl -s $ORDINIS/api/v1/dictionaries/spacecraft | jq '.scope'                 # "PUBLIC"
curl -s "$ORDINIS/api/v1/spacecraft/records?as_scope=PUBLIC" | jq '.[0].businessKey'
curl -s "$ORDINIS/api/v1/dictionaries/satellite_type/dependents" | jq 'length'  # 1 (spacecraft зависит)
curl -s "$ORDINIS/api/v1/search?q=spacecraft" | jq '.total'                   # >0
curl -s $ORDINIS/api/v1/ai/info | jq '.enabled'                               # true / false
```

## Постман collection (на случай если потерял ссылку из section 13)

27 рабочих запросов + 10-шаговый demo flow:

- `~/Desktop/Ordinis-Postman/` (если генерировал локально)
- [docs/integration/postman/](postman/) — версионный snapshot

— конец приложения A —
