Skip to content

Webhook

Танай системд нэхэмжлэх төлөгдөх, багц сунгагдах зэрэг үйлдэл болмогц Byl тэр даруйд нь таны сервер рүү POST хүсэлт илгээдэг — үүнийг webhook гэнэ. Ингэснээр та төлбөрийн төлөвийг байнга асууж шалгах (polling) шаардлагагүйгээр event болмогц мэдэж, захиалга баталгаажуулах, эрх нээх зэрэг үйлдлээ автоматжуулах боломжтой.

Ажиллах зарчим нь гуравхан алхам:

  1. Удирдлагын буланд endpoint (хүлээн авах URL) бүртгэнэ.
  2. Event болмогц бид тухайн URL руу event-ийн мэдээллийг POST хүсэлтээр илгээнэ.
  3. Таны сервер хүсэлтийн гарын үсгийг шалгаад 2xx хариу буцаана.

Endpoint бүртгэх

Удирдлагын буланд төслийнхөө Webhooks цэсээр орж endpoint-ийн URL-ээ бүртгэнэ. Нэг төсөлд идэвхжүүлсэн багцынхаа хязгаарт багтаан хэд хэдэн endpoint бүртгэж болно.

Endpoint-ийн URL дараах шаардлагыг хангасан байх ёстой:

  • https://-ээр эхэлсэн байх.
  • Интернэтээс хандах боломжтой байх — localhost болон дотоод сүлжээний хаягууд ажиллахгүй.

Хөгжүүлэлтийн орчинд туршихдаа ngrok зэрэг tunnel ашиглан локал серверээ түр хугацаанд интернэтэд нээж болно.

Хүсэлтэд хариу өгөх

Таны сервер 5 секундын дотор 2xx төлөвтэй хариу буцаавал бид хүргэлтийг амжилттайд тооцно. Өөр төлөв кодтой хариу, redirect, эсвэл хугацаа хэтэрсэн тохиолдолд амжилтгүйд тооцож дахин илгээнэ.

Эхлээд хариу буцааж, дараа нь боловсруул

Удаан үргэлжлэх ажлаа (имэйл илгээх, гадаад API дуудах гэх мэт) хариу буцаахаасаа өмнө хийвэл 5 секундын хугацаа хэтэрч, амжилттай боловсруулсан event тань амжилтгүйд тооцогдох эрсдэлтэй. Event-ээ хүлээж аваад шууд 200 буцаагаад, боловсруулалтаа дараалал (queue) дээр асинхроноор хийхийг зөвлөж байна.

Payload-ийн бүтэц

Бүх event ижил бүтэцтэй JSON байна:

ТалбарТайлбар
idEvent-ийн дахин давтагдашгүй дугаар.
project_idEvent хамаарах төслийн ID.
typeEvent-ийн төрөл (жш: invoice.paid).
objectdata.object-т ямар объект байгааг заана (жш: invoice).
data.objectEvent хамаарах объект бүтнээрээ (нэхэмжлэх, checkout гэх мэт).
created_atEvent үүссэн огноо.

Event-ийн төрөл бүрийн жишээг доороос харна уу.

Давхардсан event-ээс сэргийлэх

Ховор тохиолдолд нэг event танд нэгээс олон удаа очиж болзошгүй (жишээ нь таны сервер хариу өгсөн ч сүлжээний алдаагаар бидэнд хүрээгүй үед). Боловсруулсан event-ийн id-г хадгалж, өмнө нь ирсэн id-тэй event-ийг алгасахыг зөвлөж байна (idempotency).

Дарааллыг бүү найд

Event-үүд үүссэн дарааллаараа очих баталгаагүй — дахин илгээлтийн улмаас хожуу үүссэн event түрүүлж очих тохиолдол гардаг. Логикоо тодорхой дараалалд найдахгүйгээр бичээрэй.

Гарын үсэг шалгах

Webhook хүлээн авдаг URL тань интернэтэд нээлттэй байдаг тул хэн ч тийшээ хуурамч хүсэлт илгээж болзошгүй. Үүнээс хамгаалахын тулд бид хүсэлт бүрд Byl-Signature header-ээр гарын үсэг дагалдуулдаг — та хүсэлт бүр дээр үүнийг заавал шалгаж байхыг зөвлөж байна.

Гарын үсгийг шалгах гурван алхам:

1. Хүсэлтийн түүхий (raw) body-г ав. JSON-ыг задлаад дахин json_encode хийвэл түлхүүрийн дараалал өөрчлөгдөж гарын үсэг таарахгүй болох магадлалтай тул body-г яг ирснээр нь ашиглана.

2. Secret-ээрээ HMAC-SHA256 гарын үсэг тооцоол. Secret-ээ удирдлагын булан дахь endpoint-ийн дэлгэрэнгүй хуудасны Signing secret хэсгээс харна. Secret нь багийн бүх төсөлд ижил байна.

3. Тооцоолсон гарын үсгээ Byl-Signature header-тэй харьцуул. Харьцуулахдаа === биш, тогтмол хугацааны (constant-time) харьцуулалт ашиглана — PHP-д hash_equals(), Node.js-д crypto.timingSafeEqual().

PHP жишээ:

php
$payload = $request->getContent(); // түүхий body. Задлаад дахин encode хийхгүй.
$computedSignature = hash_hmac('sha256', $payload, $secret);

if (! hash_equals($computedSignature, $request->header('Byl-Signature'))) {
    abort(401);
}

Node.js жишээ:

js
const crypto = require("crypto");

const computedSignature = crypto
  .createHmac("sha256", secret)
  .update(rawRequestBody) // түүхий body. JSON.stringify(req.body) биш.
  .digest("hex");

const isValid = crypto.timingSafeEqual(
  Buffer.from(computedSignature),
  Buffer.from(req.headers["byl-signature"] ?? ""),
);

Framework-ийн body parser-т анхаар

Ихэнх framework (Express-ийн express.json(), Laravel гэх мэт) body-г автоматаар задалдаг тул түүхий body-г тусад нь авах тохиргоо хэрэгтэй байж болно. Жишээ нь Express-д express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }) гэж тохируулдаг.

Бүрэн ажиллах жишээ код хэрэгтэй бол дараах repository-г харна уу:

Дахин илгээлт

Хүргэлт амжилтгүй болбол бид exponential backoff зарчмаар, өсөх интервалтайгаар автоматаар дахин илгээдэг:

ОролдлогоӨмнөх оролдлогоос хойш
25 минут
330 минут
42 цаг
55 цаг
610 цаг
724 цаг
848 цаг

Өөрөөр хэлбэл эхний оролдлогоос хойш нийт 4 орчим хоногийн турш, 8 хүртэл удаа оролддог. Энэ хугацаанд аль нэг оролдлого амжилттай болбол дахин илгээлт зогсоно.

Бүх оролдлого амжилтгүй болбол хүргэлт failed төлөвт шилжиж, багийн гишүүдэд имэйлээр мэдэгдэнэ (endpoint тус бүрт хоногт нэг удаа). Асуудлаа зассаны дараа удирдлагын булангаас:

  • Нэг хүргэлтийг нээгээд Дахин илгээх товчоор дахин илгээх, эсвэл
  • Endpoint-ийн хуудасны Амжилтгүйг дахин илгээх товчоор амжилтгүй болсон бүх хүргэлтийг нэг дор дахин илгээх боломжтой.

Endpoint-ийн хуудсанд сүүлд илгээсэн хүсэлтүүд, тэдгээрийн payload, таны серверийн буцаасан хариуны төлөв код харагдана. Хүргэлтийн түүх 30 хоног хадгалагдана.

Endpoint автоматаар идэвхгүй болох

Endpoint рүү илгээсэн хүргэлтүүд 7 хоногийн турш тасралтгүй амжилтгүй болбол (энэ хугацаанд нэг ч амжилттай хүргэлтгүй бол) бид тухайн endpoint-ийг автоматаар идэвхгүй болгож, багийн гишүүдэд имэйлээр мэдэгдэнэ.

Идэвхгүй болсон endpoint-ийг удирдлагын булан дахь хуудаснаас нь Идэвхжүүлэх товчоор буцааж идэвхжүүлнэ. Идэвхжүүлмэгц хүлээгдэж байсан дахин илгээлтүүд үргэлжилнэ.

Идэвхгүй байх хугацааны event-үүд илгээгдэхгүй

Endpoint идэвхгүй байх хугацаанд болсон event-үүд тухайн endpoint рүү илгээгдэхгүй бөгөөд идэвхжүүлсний дараа ч нөхөж илгээгдэхгүй. Тиймээс идэвхгүй болсон тухай имэйл ирвэл аль болох хурдан асуудлаа засаж идэвхжүүлээрэй.

Webhook event-үүд

Дараах төрлийн event-үүдийг бид одоогоор илгээж байна.

EventТайлбар
invoice.paidНэхэмжлэх амжилттай төлөгдсөн.
invoice.voidНэхэмжлэх хүчингүй болгогдсон.
checkout.completedCheckout амжилттай төлөгдсөн.
subscription.createdШинэ багц үүссэн.
subscription.renewedБагцын мөчлөг сунгагдсан.
subscription.updatedБагц солигдсон, цуцлалт хүссэн эсвэл буцсан.
subscription.renewal_dueСунгалтын сануулга илгээгдсэн.
subscription.past_dueБагцын хугацаа хэтэрсэн (grace period).
subscription.canceledБагц бүрэн цуцлагдсан.
product.stock_lowБарааны үлдэгдэл цөөрсөн эсвэл дууссан.

invoice.paid

Нэхэмжлэх амжилттай төлөгдсөн үед invoice.paid төрөлтэй event илгээх болно. data.object-т төлөгдсөн нэхэмжлэхийн объект очих болно.

json
{
  "id": 3,
  "project_id": 1,
  "type": "invoice.paid",
  "object": "invoice",
  "data": {
    "object": {
      "id": 71,
      "amount": 10,
      "number": "TEST-0003",
      "status": "paid",
      "due_date": "2025-08-08T16:14:07.000000Z",
      "created_at": "2025-08-07T16:14:07.000000Z",
      "project_id": 1,
      "updated_at": "2025-08-07T16:14:16.000000Z",
      "description": "First invoice",
      "url": "https://byl.mn/h/invoice/5708/XN3GbRBxTslkMCeDj10CJtqlHiPfcmZ8"
    }
  },
  "created_at": "2025-08-07T16:14:16.000000Z",
  "updated_at": "2025-08-07T16:14:16.000000Z"
}

checkout.completed

Checkout амжилттай төлөгдсөн үед checkout.completed төрөлтэй event илгээх болно. data.object-т төлөгдсөн checkout объект очих болно.

json
{
  "id": 59,
  "project_id": 5,
  "type": "checkout.completed",
  "object": "checkout",
  "data": {
    "object": {
      "id": 13338,
      "project_id": 5,
      "mode": "payment",
      "status": "complete",
      "url": "https://byl.mn/h/checkout/13338/Yi7smBuk",
      "is_guest": false,
      "customer": {
        "id": 12,
        "client_reference_id": "user_842",
        "name": "Бат-Эрдэнэ"
      },
      "amount_total": "27000.000000000000",
      "amount_subtotal": "30000.000000000000",
      "customer_email": "[email protected]",
      "client_reference_id": "order_1042",
      "subscription_id": null,
      "items": [
        {
          "product": {
            "id": 7,
            "name": "Starter багц",
            "client_reference_id": "starter"
          },
          "price": {
            "id": 3,
            "type": "recurring",
            "unit_amount": 30000,
            "recurring_interval": "month",
            "recurring_interval_count": 1,
            "lookup_key": "starter_monthly"
          },
          "quantity": 1,
          "amount_unit": 30000,
          "amount_subtotal": 30000,
          "amount_total": 30000,
          "adjustable_quantity": false
        }
      ],
      "phone_number_collection": true,
      "phone_number": "99999999",
      "email_collection": true,
      "delivery_address_collection": false,
      "delivery_address": null,
      "coupon_codes": [
        {
          "code": "SUMMER2026",
          "coupon_name": "Зуны хямдрал",
          "discount_amount": "3000.00",
          "redeemed_at": "2026-08-14T09:30:00.000000Z"
        }
      ],
      "expires_at": "2026-08-14T16:00:00.000000Z",
      "created_at": "2026-08-14T09:12:00.000000Z",
      "updated_at": "2026-08-14T09:30:00.000000Z"
    }
  },
  "created_at": "2026-08-14T09:30:00.000000Z",
  "updated_at": "2026-08-14T09:30:00.000000Z"
}

Анхаарах талбарууд:

  • customer — checkout-д харилцагч холбогдоогүй (guest) бол null.
  • subscription_id — сунгалт эсвэл багц солих checkout үед л бөглөгдөнө. Шинээр багц үүсгэх checkout дээр null байх бөгөөд багц үүссэний дараа subscription.created event тусад нь ирнэ.
  • items[].price.lookup_key — үнэд өөрөө өгсөн код. Байхгүй бол null. Багцын логикоо price.id-аар биш үүгээр бичвэл үнээ дараа солиход код өөрчлөх шаардлагагүй.
  • items[].price.id болон items[].product.id — үнэ бүртгэлгүй (price_data-аар үүсгэсэн) item дээр null байна.
  • amount_total, amount_subtotal — checkout дээр decimal тул тэмдэгт мөр (string), item дээр тоо байна. amount_subtotal нь хөнгөлөлт хасахаас өмнөх дүн, amount_total нь хассаны дараах төлсөн дүн. Item-ийн дүнгүүд хөнгөлөлт хасагдаагүй байна.

coupon_codes талбар нь checkout-д хэрэглэгдсэн хөнгөлөлтийн кодуудын мэдээллийг агуулна. Хэрэв checkout-д хөнгөлөлтийн код хэрэглэгдээгүй бол энэ талбар хоосон массив байна.

coupon_codes массивын элемент бүр дараах талбаруудыг агуулна:

  • code: Хөнгөлөлтийн код
  • coupon_name: Хөнгөлөлтийн нэр
  • discount_amount: Хөнгөлөлтийн дүн
  • redeemed_at: Хөнгөлөлт хэрэглэгдсэн огноо

subscription.*

Багцын төлөвт өөрчлөлт орох бүрд subscription. угтвартай event илгээгдэнэ (төрлүүдийг дээрх хүснэгтээс харна уу). data.object-т багцын объект харилцагч, бүтээгдэхүүн, үнийн мэдээллийн хамт очно.

json
{
  "id": 87,
  "project_id": 1,
  "type": "subscription.renewed",
  "object": "subscription",
  "data": {
    "object": {
      "id": 4,
      "project_id": 1,
      "status": "active",
      "customer": {
        "id": 12,
        "client_reference_id": "user_842",
        "name": "Бат-Эрдэнэ",
        "email": "[email protected]"
      },
      "product": {
        "id": 7,
        "name": "Starter багц",
        "client_reference_id": null
      },
      "price": {
        "id": 3,
        "type": "recurring",
        "unit_amount": 30000,
        "recurring_interval": "month",
        "recurring_interval_count": 1,
        "lookup_key": "starter_monthly"
      },
      "current_period_start": "2026-07-23T04:10:00.000000Z",
      "current_period_end": "2026-08-23T15:59:59.000000Z",
      "trial_ends_at": null,
      "canceled_at": null,
      "is_test": false,
      "created_at": "2026-06-23T04:10:00.000000Z",
      "updated_at": "2026-07-23T04:10:00.000000Z"
    }
  },
  "created_at": "2026-07-23T04:10:00.000000Z",
  "updated_at": "2026-07-23T04:10:00.000000Z"
}

Хэрэглэгчийн эрхийг subscription.canceled ирэх үед хаана — цуцлалт хүссэн ч мөчлөг дуустал эрх хүчинтэй байдаг тул subscription.updated (canceled_at бөглөгдсөн) дээр эрх хаахгүй. Дэлгэрэнгүйг Subscriptions хуудаснаас уншина уу.

product.stock_low

Зарах тоо тохируулсан бүтээгдэхүүний үлдэгдэл босго давах үед (5 ба доош, 0, эсвэл сөрөг болоход) product.stock_low төрөлтэй event илгээнэ. data.object-т бүтээгдэхүүний товч мэдээлэл болон үлдсэн тоо очно.

json
{
  "id": 112,
  "project_id": 1,
  "type": "product.stock_low",
  "object": "product",
  "data": {
    "object": {
      "id": 7,
      "project_id": 1,
      "name": "Гар хийцийн ваар",
      "client_reference_id": null,
      "stock": 4
    }
  },
  "created_at": "2026-08-14T09:30:00.000000Z",
  "updated_at": "2026-08-14T09:30:00.000000Z"
}

Босго дотор давтагдсан борлуулалт бүрд дахин илгээхгүй — зөвхөн босго давах мөчид нэг удаа очно. Дэлгэрэнгүйг Барааны үлдэгдэл хуудаснаас уншина уу.