7.5 KiB
Webhooks
Альтернатива Kafka events для consumer'ов которые не хотят / не могут поддерживать Kafka client. Ordinis отправляет HTTP POST на зарегистрированный URL при изменениях справочников.
{% note info %}
Если у тебя есть Kafka access — используй события. Webhooks проще в развёртывании, но менее надёжны (HTTP retry vs Kafka durable log) и медленнее (HTTP overhead vs Kafka batch).
{% endnote %}
Регистрация подписки
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/subscriptions \
-d '{
"name": "altum-cache-invalidation",
"url": "https://altum-backend.altum.local/webhooks/ordinis",
"secret": "<HMAC_SECRET_GENERATED_BY_YOU>",
"scopes": ["PUBLIC", "INTERNAL"],
"dictionaries": ["spacecraft", "satellite_type", "instrument"],
"actions": ["created", "updated", "closed"],
"active": true
}'
Поля
| Поле | Тип | Назначение |
|---|---|---|
name |
string | Человекочитаемое имя для admin UI. |
url |
string | HTTPS endpoint. HTTP блокируется SSRF guard'ом. |
secret |
string | HMAC-SHA256 секрет для подписи payload (см. ниже). |
scopes |
array | Фильтр по scope (subset из доступных consumer'у). |
dictionaries |
array | Фильтр по dictionary names. null/[] = все доступные. |
actions |
array | created / updated / closed. null = все. |
categories |
array | Event categories: dict.record.* / dict.definition.* / dict.draft.*. null = все. (v2.2.0+) |
active |
bool | Можно временно отключить без удаления. |
Event categories (v2.2.0+)
С v2.2.0 webhook subscriber'ы могут фильтровать по семантическим категориям:
| Category | Маппит outbox events |
|---|---|
dict.record.* |
Record CRUD (default до v2.2.0): record.created / .updated / .closed |
dict.definition.* |
Schema-level: DefinitionCreated, DefinitionUpdated (inline PATCH) |
dict.draft.* |
Workflow drafts: SchemaDraftCreated, …ReviewRequested, …Decided, …Published, …Withdrawn |
Если categories=null подписчик получает все (legacy behaviour).
Frontend reviewer dashboards обычно подписываются на dict.draft.*
для notifications о новых drafts. Bundle consumers (Альтум, BI) —
на dict.record.*.
SSRF guard
Webhook URLs валидируются перед отправкой:
- Запрещено: private CIDR (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), localhost, link-local (169.254/16), metadata IPs.
- Запрещено: non-HTTPS scheme. Только
https://. - Запрещено: URL > 2048 chars.
При нарушении registration возвращает 422 + список причин.
Payload
POST на зарегистрированный URL с body:
{
"deliveryId": "wd-abc123-xyz789",
"subscriptionName": "altum-cache-invalidation",
"occurredAt": "2026-05-07T15:23:45.123+03:00",
"events": [
{
"schemaVersion": "1.0.0",
"eventType": "dictionary.changed",
"dictionary": "spacecraft",
"businessKey": "RESURS-P-3",
"action": "updated",
"version": 3,
"validFrom": "2026-05-07T00:00:00+03:00",
"data": { ... }
}
]
}
{% note tip %}
Payload может содержать несколько events (events array) если они
произошли в одной transaction (bulk close, bundle import). Не предполагай
single-event payload.
{% endnote %}
HMAC подпись
Каждый POST имеет header:
X-Ordinis-Signature: sha256=<HEX>
X-Ordinis-Delivery: wd-abc123-xyz789
X-Ordinis-Subscription: altum-cache-invalidation
Где <HEX> = HMAC-SHA256 от raw body bytes с secret который ты задал
при регистрации.
Verify (Python)
import hmac, hashlib
def verify(body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
Verify (Node.js)
import crypto from 'crypto'
function verify(body, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
)
}
{% note warning %}
Verify подпись до парсинга JSON. Если invalid — 403 Forbidden,
не трогай payload. Без verify ты уязвим к подделке events от любого, кто
знает твой URL.
{% endnote %}
Response contract
Consumer должен ответить 2xx в течение 10 секунд:
| Status | Семантика |
|---|---|
200 / 201 / 202 / 204 |
Принято. Ordinis помечает delivered. |
4xx |
Permanent failure (твой bug). Ordinis НЕ retry. |
5xx / timeout |
Transient. Ordinis retry с exponential backoff. |
Retry policy
| Попытка | Интервал |
|---|---|
| 1 | сразу |
| 2 | через 30s |
| 3 | через 2 мин |
| 4 | через 10 мин |
| 5 | через 1ч |
| 6-10 | каждые 6ч |
| После всех попыток | Ordinis team помечает delivery как failed для ручного разбора |
Если consumer недоступен > 1000 retry попыток — Ordinis team будет расследовать. Долгосрочное unavailability стоит coordinate (planned maintenance window) чтобы не накапливать orphan deliveries.
Test ping
Endpoint для проверки доступности URL до регистрации:
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/test-ping \
-d '{
"url": "https://altum-backend.altum.local/webhooks/ordinis",
"secret": "<TEST_SECRET>"
}'
Response:
{
"success": true,
"statusCode": 200,
"responseBody": "OK",
"elapsedMs": 142
}
Либо details про failure (DNS issue, timeout, SSRF reject и т.д.).
Best practices
- Idempotency — webhook может прийти дважды (network retry, partial
failure). Используй
deliveryIdкак dedup key. - Async обработка — отвечай
202 Acceptedсразу, обрабатывай в background. 10s timeout жёсткий — если БД медленная, лучше принять и сложить в очередь. - HMAC verify первым — до парсинга JSON.
- Metrics — track received / processed / errors для своего side.
- Whitelist Ordinis IP на firewall если строгие правила. Точный адрес запросить у Ordinis team при registration.
Дальше
- События (Kafka) — primary delivery channel
- Caching — webhook invalidation pattern