NurCore API
API Reference

Webhooks

Real-time события от NurCore — оплата прошла, рейс задержался, пассажир зарегистрировался.

NurCore отправляет webhooks в ваш backend, когда происходят важные события (бронь подтвердилась, рейс задержался, пассажир зарегистрировался). Это критично для интеграции — без webhooks вам пришлось бы постоянно опрашивать API ("polling каждые 60 секунд: что нового?").

Архитектура

[Пассажир оплачивает на сайте авиакомпании]


[Your backend] → POST /bookings/{id}/initiate-payment → NurCore


[Freedom Pay] → платёж проходит → webhook → NurCore


[NurCore] → booking.confirmed → POST на ваш webhook URL
        │                       (HMAC-SHA256 подпись)

[Ваш backend]
        │ Парсит payload
        │ Начисляет миль в loyalty programme
        │ Шлёт push пользователю через ваш FCM

[Возвращает 200 OK]

Регистрация webhook subscription

POST /api/v1/notifications/outgoing-webhooks/

Создаёт подписку и возвращает secret (показывается ОДИН раз — сохраните его, потом будете только rotate).

curl -X POST \
  -H "Authorization: Bearer $STAFF_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Your backend integration",
    "url": "https://api.example-airline.com/internal/nurcore-webhooks",
    "events": [
      "booking.confirmed",
      "booking.cancelled",
      "booking.refunded",
      "flight.delayed",
      "flight.cancelled",
      "passenger.checked_in"
    ],
    "max_retries": 3,
    "timeout_seconds": 10
  }' \
  "https://api.nurcore.kg/api/v1/notifications/outgoing-webhooks/"

Response 201:

{
  "id": "uuid",
  "name": "Your backend integration",
  "url": "https://api.example-airline.com/internal/nurcore-webhooks",
  "secret": "abc123XyZ...48-char-secret",
  "events": ["booking.confirmed", "..."],
  "is_active": true,
  "max_retries": 3,
  "timeout_seconds": 10,
  "created_at": "2026-05-12T02:00:00Z"
}

⚠️ Сохраните secret сразу — он показывается только при создании. Используется для проверки HMAC подписей входящих webhooks.

Управление подписками

EndpointНазначение
GET /outgoing-webhooks/Все подписки (без secrets)
GET /outgoing-webhooks/{id}Деталь подписки
PATCH /outgoing-webhooks/{id}Обновить (url, events, is_active)
DELETE /outgoing-webhooks/{id}Удалить
POST /outgoing-webhooks/{id}/rotate-secretНовый secret (старый перестаёт работать)
GET /outgoing-webhooks/{id}/deliveriesИстория доставок (debug)
GET /outgoing-webhooks/event-typesСписок всех поддерживаемых events

Поддерживаемые события

Event TypeКогдаPayload содержит
booking.createdПосле POST /bookings/ → status=PENDINGbooking_id, booking_reference, total, expiry
booking.confirmedПосле успешной оплаты → CONFIRMED+ payment_status, confirmed_at
booking.cancelledПри cancel+ cancelled_at, refund_amount (если оплачено)
booking.refundedПосле refund (отдельно от cancel)+ refund_amount, currency, refunded_at
booking.expiredPENDING → EXPIRED (15 минут истекли)booking_id, expired_at
flight.delayedЗадержка ≥30 минflight_id, original_dep, new_dep, delay_minutes
flight.cancelledОтмена рейсаflight_id, reason, affected_bookings[]
flight.divertedDiversionflight_id, new_destination, reason
flight.departedПосле takeoffflight_id, actual_departure
flight.arrivedПосле landingflight_id, actual_arrival
passenger.checked_inOCI или counter check-inbooking_id, passenger_id, seat, terminal
passenger.boardedНа gatebooking_id, passenger_id, gate, boarded_at
passenger.no_showНе явилсяbooking_id, passenger_id, flight_id

Пример payload — booking.confirmed

{
  "booking_id": "uuid",
  "booking_reference": "ABC123",
  "status": "confirmed",
  "total_amount": 5000.00,
  "currency": "KGS",
  "passenger_count": 2,
  "contact_email": "user@example.com",
  "flight_id": "uuid",
  "confirmed_at": "2026-05-12T10:00:00Z"
}

Пример payload — flight.delayed

{
  "flight_id": "uuid",
  "flight_number": "ZM-202",
  "origin_iata": "FRU",
  "destination_iata": "IST",
  "original_departure": "2026-05-15T07:30:00Z",
  "new_departure": "2026-05-15T09:00:00Z",
  "delay_minutes": 90,
  "reason": "ATC delay",
  "affected_passengers": 78
}

HTTP Headers вебхука

Каждый POST на ваш URL содержит:

POST /your/webhook/endpoint HTTP/1.1
Host: api.example-airline.com
Content-Type: application/json
User-Agent: NurCore-Webhooks/1.0
X-NurCore-Event: booking.confirmed
X-NurCore-Event-Id: 550e8400-e29b-41d4-a716-446655440000
X-NurCore-Timestamp: 1747031600
X-NurCore-Signature: sha256=abc123def456...
X-NurCore-Delivery-Id: 660e8400-e29b-41d4-a716-446655440001

{"booking_id":"uuid","booking_reference":"ABC123",...}
HeaderНазначение
X-NurCore-EventТип события (booking.confirmed)
X-NurCore-Event-IdUUID события — используйте для идемпотентности на вашей стороне
X-NurCore-TimestampUnix timestamp — нужен для signature verification
X-NurCore-SignatureHMAC-SHA256, sha256=<hex>обязательно проверяйте
X-NurCore-Delivery-IdUUID попытки доставки — для debug

Проверка подписи (CRITICAL!)

NurCore подписывает каждый webhook HMAC-SHA256. Всегда проверяйте подпись — иначе злоумышленник может прислать поддельные события.

Алгоритм

signed_payload = "{timestamp}.{event_id}.{request_body}"
expected = HMAC-SHA256(secret, signed_payload).hex()
expected_signature = "sha256=" + expected

# Сравните с X-NurCore-Signature (constant-time comparison!)

Python (FastAPI)

import hmac
import hashlib

WEBHOOK_SECRET = "abc123XyZ..."  # из POST /outgoing-webhooks/ response

@app.post("/internal/nurcore-webhooks")
async def receive_webhook(request: Request):
    body = await request.body()
    timestamp = request.headers["X-NurCore-Timestamp"]
    event_id = request.headers["X-NurCore-Event-Id"]
    signature = request.headers["X-NurCore-Signature"]

    # 1. Compute expected
    signed_payload = f"{timestamp}.{event_id}.{body.decode()}"
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()

    # 2. Constant-time compare
    if not hmac.compare_digest(expected, signature):
        raise HTTPException(403, "Invalid signature")

    # 3. Anti-replay: timestamp не старше 5 минут
    import time
    if abs(time.time() - int(timestamp)) > 300:
        raise HTTPException(400, "Replay attack: timestamp too old")

    # 4. Idempotency: уже обработали этот event_id?
    if await already_processed(event_id):
        return {"status": "duplicate"}

    # 5. Обработка
    event_type = request.headers["X-NurCore-Event"]
    data = await request.json()
    await handle_event(event_type, data)

    await mark_processed(event_id)
    return {"status": "ok"}

TypeScript (Node.js)

import crypto from "crypto";

const WEBHOOK_SECRET = process.env.NURCORE_WEBHOOK_SECRET!;

app.post("/internal/nurcore-webhooks", async (req, res) => {
  const body = JSON.stringify(req.body);  // или raw body
  const timestamp = req.headers["x-nurcore-timestamp"] as string;
  const eventId = req.headers["x-nurcore-event-id"] as string;
  const receivedSig = req.headers["x-nurcore-signature"] as string;

  const signedPayload = `${timestamp}.${eventId}.${body}`;
  const expected = "sha256=" + crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(signedPayload)
    .digest("hex");

  // Constant-time comparison
  const a = Buffer.from(expected);
  const b = Buffer.from(receivedSig);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(403).json({ error: "Invalid signature" });
  }

  if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
    return res.status(400).json({ error: "Replay attack" });
  }

  // Process event...
  res.json({ status: "ok" });
});

Retry Policy

Если ваш endpoint вернул не 2xx — NurCore попробует снова:

ПопыткаЗадержка
1 (немедленно)0s
2+1 минута
3+5 минут (от начала)
4+30 минут (от начала)

После max_retries (default 3) → статус dead_letter. Дальше повторов нет — нужно вручную через GET /outgoing-webhooks/{id}/deliveries найти и переотправить (admin feature).

Что считается transient vs permanent

Статус кодПоведение
2xx✅ delivered, retry прекращается
408, 425, 429🔄 retry (transient — rate-limit, timeout)
400, 401, 403, 404, 410, 422dead_letter сразу (ваш bug, retry бесполезен)
5xx🔄 retry
Timeout🔄 retry (default 10s — настраивается через timeout_seconds)
Connection error🔄 retry

Ваш endpoint должен

  1. Быстро отвечать (≤ 10 секунд) — иначе timeout → retry
  2. Идемпотентно обрабатывать (X-NurCore-Event-Id — primary key)
  3. Не делать heavy work синхронно — положите в очередь, ответьте 200
  4. Возвращать 4xx только при настоящих client bugs (malformed payload, missing required field)

Best practice — async обработка

@app.post("/internal/nurcore-webhooks")
async def receive_webhook(request: Request):
    # 1. Проверить подпись (быстро)
    verify_signature(request)

    # 2. Сохранить в очередь
    await queue.publish("nurcore_event", await request.json())

    # 3. Ответить 200 сразу
    return {"status": "queued"}

Дальше worker'ы парсят очередь асинхронно. Это позволяет принять 1000+ webhooks/sec без блокировки.


Идемпотентность на вашей стороне

NurCore гарантирует at-least-once delivery — webhook может прилететь 2+ раз (timeout на одном retry → следующая попытка делает duplicate). Ваш обработчик должен дедуплицировать по X-NurCore-Event-Id.

-- В вашей БД:
CREATE TABLE processed_webhook_events (
  event_id UUID PRIMARY KEY,
  processed_at TIMESTAMPTZ DEFAULT now()
);

-- В обработчике:
INSERT INTO processed_webhook_events (event_id) VALUES ($1)
ON CONFLICT DO NOTHING
RETURNING event_id;
-- Если RETURNING пустой → уже обработали, skip

Партнёрский фильтр

Если partner_id указан при создании subscription — webhook'и приходят только для броней этого партнёра:

{
  "name": "Astana Travel agency webhooks",
  "url": "https://crm.astana-travel.kz/nurcore",
  "events": ["booking.confirmed", "booking.cancelled"],
  "partner_id": "uuid-of-astana-travel"
}

При booking.confirmed от бронирования другого партнёра — этой подписке не доставится. Это позволяет настроить multiple integrations без cross-talk.


Тестирование

Локально с ngrok / Cloudflare tunnel

# Localhost listener (Python)
python3 -m http.server 8080

# Tunnel через ngrok
ngrok http 8080

# Создаёте subscription на полученный URL
curl -X POST .../outgoing-webhooks/ -d '{
  "url": "https://abc123.ngrok.io/webhook",
  "events": ["booking.confirmed"],
  ...
}'

Replay прошлых событий

# История всех попыток для подписки
curl -H "Authorization: Bearer $JWT" \
  "https://api.nurcore.kg/api/v1/notifications/outgoing-webhooks/{sub_id}/deliveries"

# Видите failed/dead_letter deliveries для debug

FAQ

Что если мой backend упал и не отвечал час?

NurCore сделает 3 retry с backoff (1m / 5m / 30m). Если все не прошли — событие в dead_letter. Можно вручную переотправить через admin API (в дашборде или curl).

Можно ли получать webhooks через webhook.site для теста?

Да, для разработки. webhook.site принимает любой POST, секрет можно поставить любой (но проверьте signature код).

Поддерживается ли webhook batch (несколько событий в одном request)?

Пока нет — каждое событие отправляется отдельным HTTP POST. Это упрощает retry и idempotency. Если объём станет проблемой — добавим batch.

Есть ли rate-limit на доставку?

Нет — webhooks доставляются как только событие происходит, без задержек. Если ваш endpoint вернул 429 → NurCore делает retry с backoff.


Связанные документы

On this page