Skip to content

Laravel SDK

Introduction

Byl's Laravel SDK provides an expressive, fluent interface for working with invoices, checkouts, customers, subscriptions, the billing portal, and webhooks in your Laravel application. If you have used Laravel Cashier before, the SDK's Billable trait will feel immediately familiar.

  • Package: kitelab-dev/byl-laravel
  • Requirements: PHP 8.2+, Laravel 11 / 12 / 13

Installation

First, install the package using Composer:

bash
composer require kitelab-dev/byl-laravel

Configuration

Next, define your Byl connection values in your application's .env file:

dotenv
BYL_TOKEN=your-api-token
BYL_PROJECT_ID=1
BYL_WEBHOOK_SECRET=your-webhook-secret
  • BYL_TOKEN — created as described on the API token page.
  • BYL_PROJECT_ID — found in your project's settings in the dashboard.
  • BYL_WEBHOOK_SECRET — the signing secret shown on the webhook detail page.

If needed, you may publish the SDK's configuration file:

bash
php artisan vendor:publish --tag=byl-config

Invoices

You may create and manage invoices using the invoices method on the Byl facade:

php
use Byl\Laravel\Facades\Byl;

$invoice = Byl::invoices()->create([
    'amount' => 25000,
    'description' => 'Membership fee',
]);

$invoice->url;      // payment page URL
$invoice->status;   // InvoiceStatus::Open
$invoice->isPaid(); // false

You may also retrieve, void, or delete an invoice:

php
Byl::invoices()->find($invoice->id);
Byl::invoices()->void($invoice->id);
Byl::invoices()->delete($invoice->id);

Invoice, checkout, and portal session objects may be returned directly from a route or controller, which will redirect the customer to the corresponding payment page:

php
public function pay(Order $order)
{
    return Byl::invoices()->createFor($order->total, "Order #{$order->id}");
}

Checkout

For purchases involving products, quantities, and discounts, you may use the checkout builder. The builder exposes every parameter of the Checkout API via fluent methods:

php
$checkout = Byl::checkouts()->builder()
    ->successUrl(route('purchase.success'))
    ->cancelUrl(route('cart'))
    ->addPrice('starter_monthly')            // lookup key
    ->addPriceId(3)                          // price ID on Byl
    ->addPriceData(15000, 'Shoes', quantity: 2)  // inline price
    ->collectPhoneNumber()
    ->collectDeliveryAddress()
    ->allowPromotionCodes()
    ->discount(5400, 'Discount')
    ->clientReferenceId("order_{$order->id}")
    ->create();

return redirect()->away($checkout->url);

When retrieving a checkout, its items and any applied coupon codes are included:

php
$checkout = Byl::checkouts()->find(13338);

$checkout->status;          // CheckoutStatus::Complete
$checkout->amountTotal;
$checkout->items;           // Collection<CheckoutItem>
$checkout->couponCodes;     // Collection<CouponCode>

Customers

The upsert method deduplicates by client_reference_id, so you may safely call it before every checkout you create:

php
$customer = Byl::customers()->upsert([
    'email' => $user->email,
    'name' => $user->name,
    'client_reference_id' => (string) $user->id,
]);

You may retrieve a customer directly using your own system's user ID:

php
$customer = Byl::customers()->findByClientReferenceId((string) $user->id);
$customer = Byl::customers()->findByClientReferenceIdOrNull((string) $user->id); // null if not found

The customer object provides convenient helper methods for checking access:

php
$customer->isSubscribed();                                // has an entitled subscription
$customer->subscriptionForLookupKey('starter_monthly');

Subscriptions

When a checkout with a recurring price is paid, a subscription is created automatically. Such checkouts require a customer_id and must contain a single item:

php
Byl::checkouts()->builder()
    ->customer($customer->id)
    ->addPrice('starter_monthly')
    ->successUrl(route('subscribe.success'))
    ->create();

When creating a checkout for a renewal or a plan change, specify the subscription using the subscription method:

php
Byl::checkouts()->builder()
    ->customer($customer->id)
    ->subscription($subscriptionId)
    ->addPrice('growth_monthly')
    ->create();

You may retrieve, list, and manage subscriptions:

php
$subscription = Byl::subscriptions()->find(4);

$subscription->status;             // SubscriptionStatus::Active
$subscription->isEntitled();       // true for every status except canceled
$subscription->currentPeriodEnd;   // when access expires (Carbon)
$subscription->cancelRequested();  // cancellation requested but period not yet ended

Byl::subscriptions()->list(['status' => 'active']);          // paginated
Byl::subscriptions()->list(['price' => 'starter_monthly']);  // by lookup key
Byl::subscriptions()->all(['status' => 'active']);           // all pages (lazy)
Byl::subscriptions()->startTrial(customerId: 12, price: 'starter_monthly', trialDays: 14);
Byl::subscriptions()->cancel(4);
Byl::subscriptions()->resume(4);

Billable trait

If you would like to work directly on your models, just as with Laravel Cashier, you may use the Billable trait. Subscriptions are stored in a local byl_subscriptions table and are automatically kept up to date whenever a webhook arrives, so calling $user->subscribed() never hits the network.

First, publish and run the SDK's migrations:

bash
php artisan vendor:publish --tag=byl-migrations
php artisan migrate

Next, specify your billable model in your .env file and add the trait:

dotenv
BYL_BILLABLE_MODEL="App\Models\User"
php
use Byl\Laravel\Billing\Billable;

class User extends Authenticatable
{
    use Billable;
}

Checking Subscription Status

You may check a user's subscription status using the following methods:

php
$user->subscribed();                      // trialing / active / past_due
$user->subscribed('starter_monthly');     // on a specific plan
$user->subscribedToProduct(7);
$user->onTrial();
$user->onGracePeriod();                   // cancellation requested but period not yet ended
$user->bylSubscription()->current_period_end;

Protecting Routes

You may use the byl.subscribed middleware to define routes that are only accessible to subscribed users:

php
Route::get('/dashboard', ...)->middleware('byl.subscribed');
Route::get('/pro', ...)->middleware('byl.subscribed:growth_monthly');

Creating Subscriptions

The newSubscription method returns a recurring checkout — the subscription is created as soon as it is paid:

php
return $user->newSubscription('starter_monthly')
    ->successUrl(route('subscribe.success'))
    ->checkout();

You may also sell multiple billing periods at once or start a free trial:

php
$user->newSubscription('starter_monthly')->cycles(3)->checkout();  // 3 billing periods at once
$user->newSubscription('starter_monthly')->startTrial(14);         // free trial

If the customer does not yet exist on Byl, these methods will upsert one using the client_reference_id and automatically store the byl_customer_id.

Managing Subscriptions

php
$subscription = $user->bylSubscription();

return $subscription->renewCheckout(3);                 // renewal
return $subscription->swapCheckout('growth_monthly');   // swap plans

$subscription->cancel();
$subscription->resume();

Differences From Cashier

Since automatic card charges are not available in Mongolia, there are no methods that charge immediately, such as swap() or renew(). Instead, the SDK returns a checkout (swapCheckout(), renewCheckout()) and the customer completes the payment themselves.

A mapping of familiar Cashier methods:

CashierByl SDK
$user->subscribed('default')$user->subscribed('starter_monthly')
$user->subscription()$user->bylSubscription()
$user->newSubscription(...)->checkout()$user->newSubscription('starter_monthly')->checkout()
->trialDays(14)->create()$user->newSubscription('starter_monthly')->startTrial(14)
$subscription->swap($price)$subscription->swapCheckout($price)
$subscription->renewCheckout($cycles)
$user->createAsStripeCustomer()$user->createOrGetBylCustomer()

Customizing the Billable Model

If you need to manually pull your Byl subscriptions into the local table, or fetch the customer's details from the API:

php
$user->syncBylSubscriptions();   // pull subscriptions from Byl into the local table
$user->asBylCustomer();          // customer details from the API

By default, the client_reference_id is the model's primary key. If you would like to use a different column or your own resolution logic:

php
// config/byl.php
'billable' => ['client_reference_column' => 'uuid'],

// or within AppServiceProvider::boot
Byl::resolveBillableUsing(fn (string $reference) => Team::where('slug', $reference)->first());

Billing Portal

To direct a user to the billing portal where they can manage their own subscription, create a session and return it directly:

php
Route::get('/billing', function () {
    $customer = Byl::customers()->findByClientReferenceId((string) auth()->id());

    return Byl::billingPortal()->createSession($customer->id);   // redirect to the portal
});

Webhooks

The SDK automatically registers a POST /byl/webhook route that verifies the Byl-Signature header and dispatches Laravel events. Register this URL when adding a webhook in the dashboard.

You may handle the events with ordinary Laravel listeners:

php
use Byl\Laravel\Events\CheckoutCompleted;
use Byl\Laravel\Events\SubscriptionCanceled;
use Illuminate\Support\Facades\Event;

Event::listen(function (CheckoutCompleted $event) {
    Order::where('id', $event->checkout->clientReferenceId)->update(['paid_at' => now()]);
});

Event::listen(function (SubscriptionCanceled $event) {
    // Revoke the user's access ONLY on this event
    User::where('id', $event->subscription->customer?->clientReferenceId)->update(['plan' => null]);
});

Each event class corresponds to a Byl event type: InvoicePaid, InvoiceVoided, CheckoutCompleted, CheckoutExpired, SubscriptionCreated, SubscriptionRenewed, SubscriptionUpdated, SubscriptionRenewalDue, SubscriptionPastDue, SubscriptionCanceled. To catch every event in one place, listen for WebhookReceived.

If you would like to change the webhook URL or use your own controller:

php
// config/byl.php → webhook.route.path = 'integrations/byl/webhook'

// or attach the signature verification to your own route
Route::post('/my/byl-webhook', MyWebhookController::class)->middleware('byl-signature');

Error Handling

Every exception thrown by the SDK implements the Byl\Laravel\Exceptions\BylException interface:

php
use Byl\Laravel\Exceptions\BylException;
use Byl\Laravel\Exceptions\ValidationException;

try {
    Byl::invoices()->create(['amount' => 5]);
} catch (ValidationException $exception) {
    $exception->errorFor('amount');   // "The amount must be at least 10."
} catch (BylException $exception) {
    report($exception);
}
ExceptionStatus
AuthenticationException401 — invalid or expired token
AuthorizationException403 — not authorized
NotFoundException404 — not found
ConflictException409 — e.g. canceling an already canceled subscription
ValidationException422 — invalid parameters
RateLimitException429 — retryAfter()
ServerException5xx
ConnectionExceptionnetwork / timeout

TIP

The SDK automatically retries requests on connection errors, 429, and 5xx responses, so you do not need to write any retry logic yourself.

Multiple Projects

When working with a project other than the default one, use the project method:

php
Byl::project(7)->invoices()->createFor(1000);
Byl::project(7, 'other-token')->customers()->find(12);

Testing

Byl::fake() returns realistically structured responses without hitting the network and allows you to make assertions against the requests that were sent:

php
$byl = Byl::fake();

$this->post('/orders', ['product' => 'starter'])->assertRedirect();

$byl->assertCheckoutCreated(fn (array $payload) => $payload['items'][0]['price'] === 'starter_monthly');

To test webhooks with a valid signature, use FakeWebhook:

php
use Byl\Laravel\Testing\FakeWebhook;

$webhook = FakeWebhook::checkoutCompleted(['id' => 13338]);

$this->postJson(route('byl.webhook'), $webhook->payload(), $webhook->headers())->assertOk();