Files

7.5 KiB
Raw Permalink Blame History

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

  1. Idempotency — webhook может прийти дважды (network retry, partial failure). Используй deliveryId как dedup key.
  2. Async обработка — отвечай 202 Accepted сразу, обрабатывай в background. 10s timeout жёсткий — если БД медленная, лучше принять и сложить в очередь.
  3. HMAC verify первым — до парсинга JSON.
  4. Metrics — track received / processed / errors для своего side.
  5. Whitelist Ordinis IP на firewall если строгие правила. Точный адрес запросить у Ordinis team при registration.

Дальше