Laravel SDK
- Introduction
- Installation
- Configuration
- Invoices
- Checkout
- Customers
- Subscriptions
- Billable trait
- Billing Portal
- Webhooks
- Error Handling
- Multiple Projects
- Testing
Introduction
Section titled “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
Section titled “Installation”First, install the package using Composer:
composer require kitelab-dev/byl-laravelConfiguration
Section titled “Configuration”Next, define your Byl connection values in your application’s .env file:
BYL_TOKEN=your-api-tokenBYL_PROJECT_ID=1BYL_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
Section titled “Invoices”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
Section titled “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
Section titled “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
Section titled “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']); // paginatedByl::subscriptions()->list(['price' => 'starter_monthly']); // by lookup keyByl::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
Section titled “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-migrationsphp 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
Section titled “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
Section titled “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
Section titled “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
Section titled “Managing Subscriptions”$subscription = $user->bylSubscription();
return $subscription->renewCheckout(3); // renewalreturn $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() |
Customizing the Billable Model
Section titled “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:
'billable' => ['client_reference_column' => 'uuid'],
// or within AppServiceProvider::bootByl::resolveBillableUsing(fn (string $reference) => Team::where('slug', $reference)->first());Billing Portal
Section titled “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
Section titled “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 routeRoute::post('/my/byl-webhook', MyWebhookController::class)->middleware('byl-signature');Error Handling
Section titled “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 |
Multiple Projects
Section titled “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
Section titled “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();