Laravel SDK
- Introduction
- Installation
- Configuration
- Invoices
- Checkout
- Customers
- Subscriptions
- Billable trait
- Billing Portal
- Webhooks
- Error Handling
- Multiple Projects
- Testing
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:
composer require kitelab-dev/byl-laravelConfiguration
Next, define your Byl connection values in your application's .env file:
BYL_TOKEN=your-api-token
BYL_PROJECT_ID=1
BYL_WEBHOOK_SECRET=your-webhook-secretBYL_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:
php artisan vendor:publish --tag=byl-configInvoices
You may create and manage invoices using the invoices method on the Byl facade:
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(); // falseYou may also retrieve, void, or delete an invoice:
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:
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:
$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:
$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:
$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:
$customer = Byl::customers()->findByClientReferenceId((string) $user->id);
$customer = Byl::customers()->findByClientReferenceIdOrNull((string) $user->id); // null if not foundThe customer object provides convenient helper methods for checking access:
$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:
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:
Byl::checkouts()->builder()
->customer($customer->id)
->subscription($subscriptionId)
->addPrice('growth_monthly')
->create();You may retrieve, list, and manage subscriptions:
$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:
php artisan vendor:publish --tag=byl-migrations
php artisan migrateNext, specify your billable model in your .env file and add the trait:
BYL_BILLABLE_MODEL="App\Models\User"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:
$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:
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:
return $user->newSubscription('starter_monthly')
->successUrl(route('subscribe.success'))
->checkout();You may also sell multiple billing periods at once or start a free trial:
$user->newSubscription('starter_monthly')->cycles(3)->checkout(); // 3 billing periods at once
$user->newSubscription('starter_monthly')->startTrial(14); // free trialIf 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
$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:
| Cashier | Byl 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:
$user->syncBylSubscriptions(); // pull subscriptions from Byl into the local table
$user->asBylCustomer(); // customer details from the APIBy 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:
// 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:
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:
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:
// 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:
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);
}| Exception | Status |
|---|---|
AuthenticationException | 401 — invalid or expired token |
AuthorizationException | 403 — not authorized |
NotFoundException | 404 — not found |
ConflictException | 409 — e.g. canceling an already canceled subscription |
ValidationException | 422 — invalid parameters |
RateLimitException | 429 — retryAfter() |
ServerException | 5xx |
ConnectionException | network / 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:
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:
$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:
use Byl\Laravel\Testing\FakeWebhook;
$webhook = FakeWebhook::checkoutCompleted(['id' => 13338]);
$this->postJson(route('byl.webhook'), $webhook->payload(), $webhook->headers())->assertOk();