Skip to content

Laravel SDK

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

First, install the package using Composer:

Терминал
composer require kitelab-dev/byl-laravel

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-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:

Терминал
php artisan vendor:publish --tag=byl-config

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(); // false

You 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}");
}

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>

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 found

The customer object provides convenient helper methods for checking access:

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

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);

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 migrate

Next, 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;
}

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;

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');

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 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.

$subscription = $user->bylSubscription();
return $subscription->renewCheckout(3); // renewal
return $subscription->swapCheckout('growth_monthly'); // swap plans
$subscription->cancel();
$subscription->resume();

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()

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 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:

config/byl.php
'billable' => ['client_reference_column' => 'uuid'],
// or within AppServiceProvider::boot
Byl::resolveBillableUsing(fn (string $reference) => Team::where('slug', $reference)->first());

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
});

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');

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

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);

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();