# 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://auth.nstart.space/realms/nstart/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=` (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 { 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 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://auth.nstart.space/realms/nstart/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= 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= 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 —