Files
mdm-ordinis/docs/integration/api-quickstart.md
T

875 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 —