Skip to content

Webhook

Byl delivers every new event to your backend in real time via a webhook request.

Every request is cryptographically signed, so you can easily verify that it genuinely came from us.

Webhooks are delivered as POST requests whose payload contains the event type and the full object the event relates to.

Payload structure

Every event is JSON with the same structure:

FieldDescription
idThe event's unique identifier.
project_idID of the project the event belongs to.
typeThe event type (e.g. invoice.paid).
objectIndicates what kind of object is in data.object (e.g. invoice).
data.objectThe full object the event relates to (an invoice, a checkout, etc.).
created_atWhen the event was created.

Guarding against duplicate events

In rare cases the same event may be delivered to you more than once (for example, when your server responded but the response never reached us due to a network error). We recommend storing the id of each processed event and skipping events whose id you have already seen (idempotency).

Configuring webhooks for a project

Register a new webhook under your project's Webhook menu in the dashboard. You can configure as many webhooks per project as your plan's limit allows.

Any URL will work, but we recommend using an https-secured URL.

Security and signatures

Every webhook request includes a Byl-Signature header — a unique signature for that event.

We generate the signature like this:

php
$computedSignature = hash_hmac('sha256', $payload, $secret);

Your backend should therefore recompute the signature for the incoming event and compare it to the one in the Byl-Signature header using a strict === equality check.

You can find the signing secret on the webhook detail page in the dashboard. The secret is the same across all of your team's projects.

To protect against malicious third parties, we recommend verifying every request.

Example signature generation code in Node.js:

js
const crypto = require("crypto");

let secret = "your_secret_here";
let payload = "your_payload_here";
let computedSignature = crypto
  .createHmac("sha256", secret)
  .update(payload)
  .digest("hex");

Example app

If you need sample integration code, see the following repository:

Failed webhook requests

A webhook delivery is considered successful when your server responds with HTTP/200. Any other response, or a timeout, is treated as a failure and the request is retried 1 hour later.

After 3 failed attempts in total, we stop retrying. Details about failed requests are available in the dashboard. Once you have fixed the issue, click the resend button.

In the dashboard you can see recently sent requests, their payloads, and the responses your server returned.

Webhook events

We currently send the following event types.

EventDescription
invoice.paidAn invoice was successfully paid.
invoice.voidAn invoice was voided.
checkout.completedA checkout was successfully paid.
subscription.createdA new subscription was created.
subscription.renewedA subscription's billing period was renewed.
subscription.updatedThe plan was changed, or cancellation was requested or reversed.
subscription.renewal_dueA renewal reminder was sent.
subscription.past_dueThe subscription is overdue (grace period).
subscription.canceledThe subscription was fully canceled.

invoice.paid

When an invoice is successfully paid, an event of type invoice.paid is sent. data.object contains the paid invoice 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

When a checkout is successfully paid, an event of type checkout.completed is sent. data.object contains the paid checkout object.

json
{
  "id": 59,
  "project_id": 5,
  "type": "checkout.completed",
  "object": "checkout",
  "data": {
    "object": {
      "id": 13338,
      "url": "https://byl.mn/h/checkout/13338/Yi7smBuk",
      "items": [
        {
          "id": 69,
          "price": {
            "id": 17,
            "product": {
              "id": 15,
              "name": "Product 1",
              "created_at": "2025-08-03T10:15:50.000000Z",
              "project_id": 5,
              "updated_at": "2025-08-03T10:15:50.000000Z",
              "client_reference_id": null
            },
            "created_at": "2025-08-03T10:17:07.000000Z",
            "product_id": 15,
            "updated_at": "2025-08-03T10:17:07.000000Z",
            "unit_amount": 50
          },
          "price_id": 17,
          "quantity": 1,
          "created_at": "2025-08-03T10:17:07.000000Z",
          "updated_at": "2025-08-03T10:17:07.000000Z",
          "amount_unit": 50,
          "checkout_id": 64,
          "amount_total": 50,
          "amount_subtotal": 50
        }
      ],
      "status": "complete",
      "is_guest": true,
      "cancel_url": null,
      "created_at": "2025-08-03T10:17:07.000000Z",
      "expires_at": "2025-08-03T16:00:00.000000Z",
      "project_id": 5,
      "receipt_id": null,
      "updated_at": "2025-08-03T10:18:43.000000Z",
      "customer_id": null,
      "success_url": "https://will.iam/success",
      "amount_total": 50,
      "phone_number": "99999999",
      "customer_email": "[email protected]",
      "amount_subtotal": 50,
      "payment_method": "qpay",
      "client_reference_id": null,
      "phone_number_collection": true,
      "coupon_codes": [
        {
          "code": "SUMMER2025",
          "coupon_name": "Summer Sale",
          "discount_amount": "10.00",
          "redeemed_at": "2025-08-03T10:18:43.000000Z"
        }
      ]
    }
  },
  "created_at": "2025-08-03T10:18:43.000000Z",
  "updated_at": "2025-08-03T10:18:43.000000Z"
}

The coupon_codes field contains details of the promotion codes applied to the checkout. If no promotion code was used, this field is an empty array.

Each element of the coupon_codes array contains the following fields:

  • code: The promotion code
  • coupon_name: The coupon's name
  • discount_amount: The discount amount
  • redeemed_at: When the code was redeemed

subscription.*

Whenever a subscription's state changes, an event prefixed with subscription. is sent (see the table above for the types). data.object contains the subscription object along with customer, product, and price details.

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
      },
      "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"
}

Revoke the customer's access when subscription.canceled arrives — access remains valid until the end of the billing period even after cancellation is requested, so do not revoke access on subscription.updated (with canceled_at populated). Read more on the Subscriptions page.