novu/novu-laravel
Official Laravel SDK for Novu, the open-source notification infrastructure. Send multi-channel notifications, manage subscribers and workflows, and trigger events from your Laravel app. Deliver in-app, email, SMS, push, and chat messages through one unified, developer-friendly API.
Install the package and publish its config in one go:
composer require novu/novu-laravel
php artisan vendor:publish --provider="Novu\Laravel\NovuServiceProvider"
Add the two credentials Novu's dashboard exposes to .env:
NOVU_API_KEY=your_api_key
NOVU_APPLICATION_ID=your_app_id
NOVU_API_URL=https://api.novu.co # optional, override for self-hosted
The NOVU_APPLICATION_ID is used as a header on every request and is easy to omit on first run — if triggers return 401/403 even with a valid API key, check that both values are set.
Where to look first, in this order:
config/novu.php — confirms which env keys the package actually reads.src/Novu.php (or equivalent client class) — the real public API; the README is thin, this file is the source of truth.src/Facades/Novu.php — the facade accessor string tells you what gets resolved.First use case: fire a workflow from a controller or queued listener. Create the workflow in the Novu dashboard, note its trigger identifier, then:
use Novu\Laravel\Facades\Novu;
Novu::trigger('order-shipped', [
'orderId' => $order->id,
'trackingUrl' => $order->tracking_url,
], $user->id);
That is the 90% case — everything else (subscribers, topics, feeds, bulk) is an elaboration.
Trigger with an inline subscriber payload. Rather than pre-registering every user in Novu first, pass subscriber data on the trigger and let Novu upsert:
Novu::trigger('welcome', [
'firstName' => $user->name,
// payload under whatever key the workflow expects
], $user->id);
Keep the subscriber ID stable (your users.id, not email) so later updates don't create duplicates.
Subscriber-scoped builder. For anything beyond a one-shot trigger, use the subscriber builder so calls read naturally:
Novu::subscriber($user->email)
->update(['firstName' => $user->name, 'phone' => $user->phone]);
Novu::subscriber($user->email)
->trigger('password-reset', ['token' => $token]);
Doing subscriber updates up front means the workflow template has real {{firstName}} values to render — a common source of empty emails.
Wire it into Laravel's event/queue system. The natural fit is a listener that reacts to a domain event and dispatches the Novu trigger. Because Novu already handles channel fan-out and retries, keep the listener thin — do not retry inside PHP:
class SendOrderShippedNotification implements ShouldQueue
{
public function handle(OrderShipped $event): void
{
Novu::subscriber($event->order->user_id)
->trigger('order-shipped', ['orderId' => $event->order->id]);
}
}
If the Laravel queue is also retrying, you'll double-send — set $tries = 1 or use a ShouldBeUnique job.
Topics for broadcast patterns. For "notify everyone on this plan" or "all members of this project," add subscribers to a topic once and trigger against the topic:
Novu::topic('pro-plan-users')->addSubscriber($user->id);
Novu::topic('pro-plan-users')->trigger('billing-reminder', ['month' => $month]);
This avoids looping over users in your app.
Bulk triggers (e.g. digest crons) are worth using when available — one HTTP round trip instead of N, and Novu batches the internal work.
Testing. Point NOVU_API_URL at a local Novu instance (npx novu@latest dev runs a tunnel/local API) in .env.testing and call against fixture workflow IDs. Since the package is a thin HTTP wrapper, Http::fake() around the client class works well for unit tests — assert the outbound request payload rather than mocking Novu's full SDK.
The package is archived. Last release was April 2024, the repo is archived, and there are essentially zero dependents. Treat it as a stable-but-frozen wrapper: it will keep working as long as Novu's HTTP API is compatible, but you will not get fixes for API changes, PHP version bumps, or new Laravel features. Budget for vendoring src/ into your own namespace, or writing a ~200-line thin client against Illuminate\Support\Facades\Http if the package ever breaks — the surface area is small enough that this is a realistic exit path.
Pin the version explicitly. Because there are no future releases, "novu/novu-laravel": "^1.0" is effectively a lock. Pin it in composer.json so a transitive dependency update doesn't silently surface a bug you have to patch yourself.
Secrets belong in env, never in config. The API key authenticates as your whole Novu environment. Rotate it in the Novu dashboard if it ever lands in a log; check that config/novu.php reads from env() and that you have config:cache in deploy scripts (the config file is cached, so env() calls outside config will return null in production).
Error handling is your responsibility. The SDK wraps an HTTP call; failure modes are 4xx (bad payload, missing workflow, unregistered subscriber) and 5xx/timeouts. Wrap triggers in try/catch inside queued listeners so a Novu outage doesn't kill the job — log the failing event and move on, or re-queue with backoff. Do not let a notification error roll back a database transaction.
Response shape varies by endpoint. trigger returns a transaction ID; bulkTrigger returns an array of them; subscriber methods return the subscriber object. Read the raw response with ->json() or the package's accessor when you need to correlate the Novu transaction ID with your own logs — this is what you'll want when debugging "the user says they didn't get the email."
Workflow identifier vs. workflow name. Trigger calls take the trigger identifier (a slug like order-shipped), not the human-readable name in the dashboard. A mismatch yields a 404 that looks like an auth problem. Double-check the identifier in Novu → Workflows → the workflow's settings panel.
Self-hosted Novu needs NOVU_API_URL. If you're running the OSS stack, point the package at your instance and ensure the API key is for that instance, not the cloud. The NOVU_APPLICATION_ID must match the self-hosted environment's application.
Extension points are minimal by design. The facade resolves a single class; if you need custom retry/backoff, request logging, or OpenTelemetry spans around Novu calls, don't fight the package — bind your own wrapper in a service provider and have it delegate to the Novu client. That keeps your integration points centralized when a future Novu API change inevitably needs handling.
How can I help you explore Laravel packages today?