ivkos/pushbullet
PHP client library for the Pushbullet API that lets apps send push notifications, links, files and SMS to phones, tablets and desktops, and manage devices, contacts and channels. A simple, framework-agnostic wrapper around Pushbullet's REST endpoints, installable via Composer.
Install the package like any Composer dependency, then grab an access token from pushbullet.com → Settings → Account → Access Tokens:
composer require ivkos/pushbullet
# .env
PUSHBULLET_TOKEN=o.xxxxxxxxxxxxxxxxxxxxxxxxxxxx
// config/services.php
'pushbullet' => [
'token' => env('PUSHBULLET_TOKEN'),
'device' => env('PUSHBULLET_DEVICE'), // optional default target iden
],
The first useful thing you can do is a smoke test from php artisan tinker:
use Pushbullet\Pushbullet;
$pb = new Pushbullet(config('services.pushbullet.token'));
$pb->pushNote('Hello from Laravel', 'Deployed ' . config('app.env') . ' at ' . now());
That call proves three things at once: token validity, outbound HTTPS/curl from your host, and that the Pushbullet account can receive pushes. From there, discover the iden of the device you actually want to target — you rarely want "all devices" in production:
foreach ($pb->devices() as $device) {
// dump($device->iden, $device->nickname, $device->active);
}
The library has no Laravel auto-discovery, service provider, or facade. That's not a defect, it's just the deal: it's a plain SDK. Where to look first in the repo: src/ for the Pushbullet class and the Device / Push / Channel value objects, the Exceptions/ folder to see what can be thrown, and the README for the full push-type list (pushNote, pushLink, pushFile, pushSMS, list management on devices/channels/contacts).
First real use case for a Laravel app: a queued "ops alert" — failed queue job, failed scheduled command, or an exception in the handler that should ping your phone rather than your inbox. Register it as a singleton so the client (and its curl handle behavior) is reused within a request:
// app/Providers/AppServiceProvider.php
$this->app->singleton(\Pushbullet\Pushbullet::class, fn () =>
new \Pushbullet\Pushbullet(config('services.pushbullet.token'))
);
Wrap it, don't sprinkle it. Because there's no transport abstraction, the single most valuable pattern is a thin adapter interface so the SDK never appears in your domain code:
namespace App\Notifications;
interface MobileNotifier
{
public function note(string $title, string $body, ?string $deviceIden = null): void;
public function link(string $url, string $title, string $body, ?string $deviceIden = null): void;
}
namespace App\Notifications;
use Pushbullet\Pushbullet;
use Pushbullet\Exceptions\PushbulletException;
class PushbulletNotifier implements MobileNotifier
{
public function __construct(private Pushbullet $client, private ?string $defaultDevice = null) {}
public function note(string $title, string $body, ?string $deviceIden = null): void
{
$target = $deviceIden ?? $this->defaultDevice;
$target
? $this->client->device($target)->pushNote($title, $body)
: $this->client->pushNote($title, $body);
}
public function link(string $url, string $title, string $body, ?string $deviceIden = null): void
{
$target = $deviceIden ?? $this->defaultDevice;
$target
? $this->client->device($target)->pushLink($url, $title, $body)
: $this->client->pushLink($url, $title, $body);
}
}
Bind the interface, and in testing bind a FakeNotifier that collects calls. This is the only reliable way to test code that sends pushes — see Gotchas.
Always send from a queued job. Pushbullet is a third-party HTTPS round trip; a slow or hanging connection must never sit in a web request or in a Schedule::run() critical path.
class SendMobileAlert implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30;
public int $timeout = 15;
public function __construct(
public string $title,
public string $body,
public ?string $deviceIden = null,
public bool $critical = false,
) {}
public function handle(MobileNotifier $notifier): void
{
$notifier->note($this->title, $this->body, $this->deviceIden);
}
public function failed(\Throwable $e): void
{
// The push channel failed; fall back to mail/Slack rather than looping.
Log::error('Pushbullet delivery failed', ['title' => $this->title, 'error' => $e->getMessage()]);
}
}
Fan-out by intent, not by "all devices". The account-level $pb->pushNote(...) broadcasts to every active device, which is right for "prod is down" and wrong for "your report is ready". Use $pb->device($iden)->pushNote(...) for personal/team-specific noise, and a channel when you have subscribers:
$pb->channel('deploys')->pushNote('v2.14.0 shipped', 'Rolled out to production.');
Hook it into the exception handler and the scheduler. A single report() override with a RateLimiter guard is the highest-signal-per-line integration you'll write — you get paged for real 5xx bursts without a single push per exception.
public function report(Throwable $e): void
{
parent::report($e);
if ($this->shouldAlert($e) && RateLimiter::attempt('pb-alert', 5, fn () => true, 300)) {
SendMobileAlert::dispatch(
'[' . config('app.env') . '] ' . class_basename($e),
Str::limit($e->getMessage(), 300)
);
}
}
Reading side. If you want an inbox view, $pb->pushes() returns recent pushes (cursor-paginated via modified_after/cursor — loop manually), and $pb->deletePush($iden) cleans up so you don't accumulate thousands of records. If you keep push idens in your own table, you can dismiss programmatically later.
Http::fake() will not intercept this library. It talks to the API via its own curl code, not Guzzle or Laravel's HTTP client. Any test that relies on Http::fake() around a Pushbullet call will hit the real network. The adapter interface above is not optional style advice — it's the testing strategy.
Catch Pushbullet\Exceptions\PushbulletException. Every non-2xx response surfaces as an exception, and the common codes are diagnostically distinct: 401 = bad/revoked token, 400 = the push type isn't supported for that target (SMS to a non-Android or SMS-permission-less device is the classic), 429 = rate limited. Wrap calls and translate; don't let a notification failure 500 a user-facing request.
Rate limits are per token and lower than you'd guess. Do not push from inside loops over collections. Aggregate into one message, or dispatch jobs with backoff. A foreach ($users as $u) { $pb->pushNote(...) } will get throttled quickly.
Idens over nicknames. Device nicknames get edited by users ("Pixel 8" → "work phone"), idens don't. Resolve idens once, put them in config/.env, and log the nickname alongside if you need humans to understand logs.
No configurable timeouts or base URL. The endpoint is hard-coded and curl options aren't exposed. Consequences: a hung connection eats your worker's timeout, and you cannot point it at a mock server. Mitigate with queued jobs (job timeout + retry_after), and if you truly need transport control, subclass the client and override the protected request method, or drop the wrapper and call POST /v2/pushes with Laravel's HTTP client — the API surface you actually use is tiny.
Token hygiene. Never dd(config('services.pushbullet.token')) in a shared terminal, and don't put the token in an exception context array. It goes in the Access-Token header; keep it out of logs, Sentry breadcrumbs, and support tickets.
It's unmaintained since 2016. Pin the exact version in composer.json. If Composer refuses to install on a recent PHP due to the php constraint, the code itself is plain curl and typically fine — widening the constraint in a fork is usually the whole fix, and worth it versus abandoning the wrapper. Keep the adapter boundary so a future swap to raw HTTP is a one-file change.
File pushes need real paths. pushFile uploads multipart; pass an absolute, readable path (storage_path(...)), not a Storage disk key. Large files risk timeouts you can't tune — prefer pushLink to a signed URL for anything sizable.
Timestamps are float epoch seconds, not strings or DateTime. Convert explicitly before storing, or you'll get surprising Carbon behavior.
Extension points are thin by design: value objects (Device, Push, Channel) for reads, subclassing for transport tweaks, and the adapter for everything else. Lean on the adapter; treat the SDK as an implementation detail you could replace in an afternoon.
How can I help you explore Laravel packages today?