bentonow/bento-laravel-sdk
A Laravel SDK for Bento that makes it easy to send events, manage subscribers, and trigger transactional emails and automations from your PHP app with a clean, fluent, Laravel-friendly API.
Install the package and publish its config:
composer install bentonow/bento-laravel-sdk
php artisan vendor:publish --provider="Bento\BentoServiceProvider" --tag="config"
That drops config/bento.php in place. Wire it from .env so you never commit secrets:
BENTO_SITE_UUID=your-site-uuid
BENTO_PUBLISHABLE_KEY=pk_...
BENTO_SECRET_KEY=sk_...
The service provider and Bento facade are auto-discovered — there is no manual registration step in config/app.php on Laravel 8+.
Where to look first:
config/bento.php — confirm the env keys the package actually reads (key names differ slightly between releases; trust the config file over the README).vendor/bentonow/bento-laravel-sdk/src/Facade/Bento.php — the docblock lists every proxied method and is the most reliable API reference you have.bentonow/bento-php-sdk client — the Laravel wrapper is thin, so method signatures and return shapes come from there.First use case: identify a newly registered user and fire a signup event. Drop this into your registration listener:
use Bento\Bento;
Bento::identify($user->email, [
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'plan' => 'free',
]);
Bento::track($user->email, 'signup', [
'source' => 'organic',
]);
If that lands in the Bento dashboard under the subscriber, everything else (tags, transactional email, purchase tracking) is the same two calls with different arguments.
The facade is convenient inside controllers and Blade, but prefer injection in services so the class is testable and the dependency is explicit:
use Bento\Bento;
class OnboardingService
{
public function __construct(private Bento $bento) {}
public function welcome(User $user): void
{
$this->bento->identify($user->email, ['plan' => $user->plan]);
}
}
The facade resolves the same singleton from the container, so both paths hit the same client instance configured from config/bento.php.
Every SDK call is a synchronous HTTP request to Bento's API. Never call it inline in a request that also does DB writes — a slow or failing Bento API turns into a slow checkout.
// In your controller/service, after the transaction commits
dispatch(new SyncSubscriberToBento($user))->afterCommit();
Use afterCommit() (or fire from a model event) so you don't identify a user whose registration rolls back.
Centralise the identify calls rather than sprinkling them across controllers:
protected static function booted(): void
{
static::created(fn (User $u) => SyncSubscriberToBento::dispatch($u));
static::updated(fn (User $u) => SyncSubscriberToBento::dispatchIf($u->wasChanged(['name', 'plan']), $u));
}
The job then does a single identify() with the full field payload. Bento treats identify as upsert-on-email, so repeated calls are safe.
Tags are the cheapest way to drive automations in Bento. Treat them as idempotent add/remove operations:
Bento::addTag($user->email, 'trial');
Bento::removeTag($user->email, 'trial');
Bento::addTag($user->email, 'paid');
Wrap add/remove tag pairs in a single job so a plan change doesn't leave a user with both trial and paid.
Bento's purchase tracking expects a specific payload shape (unique_key, value, currency, plus cart details). Build it in one place:
Bento::trackPurchase($order->user->email, [
'unique_key' => (string) $order->id, // your idempotency handle
'value' => $order->total_cents / 100,
'currency' => $order->currency,
'cart' => $order->items->map(fn ($i) => [
'product_sku' => $i->sku,
'product_name' => $i->name,
'quantity' => $i->quantity,
'price' => $i->unit_price,
])->all(),
]);
unique_key is the only thing preventing duplicate revenue in reports — always set it to a stable order identifier.
Bento hosts the templates, so your Laravel app only sends the trigger and the field payload:
Bento::sendEmail(
$user->email,
'Welcome to Acme', // subject
view('emails.welcome', compact('user'))->render(), // html body
config('mail.from.address'),
);
If you're using Bento-hosted templates rather than Laravel views, keep the HTML minimal and let Bento's template variables do the work — merge fields must exist on the subscriber (run identify() first).
The SDK talks over Guzzle directly, so Http::fake() will not intercept it. Fake at the facade/binding level instead:
Bento::shouldReceive('identify')->once()->with($user->email, Mockery::type('array'));
Bento::shouldReceive('track')->once();
For integration-style tests, bind a fake implementation of the client into the container in a base test case so nothing leaves the suite.
Custom fields must exist in Bento first. identify() with an unknown field is silently dropped or errors depending on API version — create the field in the Bento dashboard before shipping code that writes it. Keep field names in a PHP enum or constant to avoid typos.
site_uuid is mandatory and easy to miss. Config with only the publishable/secret key will fail at request time, not at boot. If you see 401/404 responses, check this first.
Don't call Bento inside a DB transaction. As above — the HTTP call may succeed while your write rolls back, leaving Bento with a subscriber that doesn't exist in your app. afterCommit() is the fix.
Rate limits and retries. The wrapper doesn't add retry logic. Configure your queue worker with --tries=3 and a backoff, and make the job implement ShouldBeUnique on the user/order id to avoid duplicate identify storms during backfills.
Facade method names track the upstream client, not Laravel conventions. If a method you expect (subscribe, unsubscribe) isn't there, grep the facade docblock — the wrapper typically exposes the raw client method names (addSubscriber, removeSubscriber, addTag).
Config caching. php artisan config:cache bakes the Bento keys in. If you rotate keys in production you must re-run config:cache (or config:clear) — a common "why is it still using the old key" trap.
Extension points. For anything the wrapper doesn't cover, resolve the underlying client from the container and use it directly, or fork the config to add a custom Guzzle client. Avoid editing vendor/ — put an adapter class in app/Services/Bento that wraps the facade so the rest of your code has a stable interface if you ever swap providers.
Observability. Log the email + event name (never the secret key) on failure in the job's failed() method. Bento's API returns useful error bodies — capture $e->getMessage() so support can diagnose without digging through queue payloads.
Unsubscribe handling. Don't try to remove subscribers on unsubscribe — use Bento's unsubscribe mechanism and reflect the state in your own DB. Hard-deleting a subscriber also deletes their event history, which breaks revenue attribution.
How can I help you explore Laravel packages today?