laravel/slack-notification-channel
Official Laravel package for sending notifications to Slack, supporting webhooks and bot tokens, interactive messages, attachments, and rich block formatting through Laravel's built-in notification system.
The channel is usually already present in a Laravel app (composer show laravel/slack-notification-channel); otherwise composer require laravel/slack-notification-channel. There is nothing to publish — the package registers itself via its service provider and hooks into Laravel's Notification system.
Two transports, two config paths:
// config/services.php
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), // SlackChannel (Web API)
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
'slack-webhook' => [
'webhook_url' => env('SLACK_WEBHOOK_URL'), // SlackWebhookChannel
],
Use the bot token (chat:write, plus chat:write.public or an /invite to the channel) when you need per-message channels, threads, or replies. Use a webhook URL when you just need "post into one channel" with zero OAuth setup.
Where to look first: the Illuminate\Notifications\Messages\SlackMessage class — that is the entire DSL (content, attachments, blocks, routing, HTTP hooks). Transport behavior lives in SlackChannel (Web API) and SlackWebhookChannel. The canonical docs are on laravel.com/docs/notifications#slack-notifications, not the repo README.
First use case — an on-demand alert without a Notifiable model:
Notification::route('slack', '#ops-alerts')
->notify(new DeployFailed($release));
class DeployFailed extends Notification
{
public function via($notifiable) { return ['slack']; }
public function toSlack($notifiable)
{
return (new SlackMessage)
->error()
->from('Deploy Bot', ':robot_face:')
->content("Deploy *{$this->release}* failed on `{$this->env}`");
}
}
Per-notifiable routing. Add routeNotificationForSlack() to your model (or User) returning a channel ID (C0123ABCD), a DM user ID (U0123ABCD), or #channel-name. Always prefer channel IDs — renaming a channel silently breaks name-based routing.
public function routeNotificationForSlack($notification)
{
return $this->slack_channel_id ?? config('services.slack.notifications.channel');
}
Blocks over attachments. The v3 line is built around Block Kit. Compose with the fluent block helpers and let the package assemble the payload:
return (new SlackMessage)
->from('Monitoring', ':satellite:')
->headerBlock('Nightly job summary')
->sectionBlock(fn ($block) => $block->fields([
"*Passed:* {$stats['passed']}",
"*Failed:* {$stats['failed']}",
]))
->dividerBlock()
->sectionBlock(fn ($block) => $block->text('Failures need triage.')->accessory(
SlackButtonElement::create('View logs')->url($url)->style('primary')
))
->contextBlock(fn ($block) => $block->text("Run at {$now}"));
Block types available include header, section (with fields and accessory), divider, context, image, actions (buttons, selects, users-select — added in v3.5–v3.7), so most Block Kit Builder layouts are reproducible fluently. If you'd rather design visually, v3.4.0 added support for hydrating a message from JSON copied out of the Block Kit Builder, and later versions expose a Block Kit Builder URL from a SlackMessage — check the exact static method signature in your installed version before relying on it.
Plain content() still works and is the right tool for one-liners; reach for blocks when you need structure, and legacy attachments() only for old workflows (Slack still renders them, but they appear below blocks).
Conditional messages. Since v3.0.1 you can branch inside the fluent chain, which keeps toSlack() free of if blocks:
return (new SlackMessage)
->content('Backup finished')
->when($stats['failed'] > 0, fn ($m) => $m->error()->content('Backup finished with failures'))
->unless($notifiable->is_quiet_hours, fn ($m) => $m->channel('#ops'));
Threading. Capture the parent ts from Slack (an incoming event or the API response) and reply with ->threadTimestamp($ts), so follow-ups stay out of the main channel.
Queue it. Slack timeouts and rate limits should never fail a business transaction. Implement ShouldQueue on the notification, and for high-volume alerts add job middleware or backoff() — the package does not implement retry/backoff for Slack's 429s.
Testing. Notification::fake() plus assertSentTo($user, DeployFailed::class, fn ($n, $channels, $notification) => ...) verifies routing and channel selection without HTTP. To assert payload shape, call the message's toArray()/buildJsonPayload() (public since v3.0.0) directly and snapshot it — that catches malformed block JSON before it hits Slack. For end-to-end behavior use Http::fake() and assert the request body.
Extending. Subclass SlackMessage and override buildJsonPayload() (public API since v3.0.0) for org-wide defaults like icon_emoji, unfurl_links, or a standard footer context block. Bind a configured Guzzle client in the container if you need proxies, timeouts, or a mock handler in tests.
chat.postMessage returns HTTP 200 with {"ok": false, "error": "channel_not_found"}. Nothing throws. Log the response or assert ok in your own wrapper, otherwise "my notification isn't arriving" becomes an hour of debugging. Webhooks behave differently — a bad payload returns 400 and a Guzzle exception does surface.routeNotificationForSlack() returns null and the message has no channel(), you're relying on services.slack.notifications.channel; a misconfigured fallback produces failed sends rather than a skipped notification. Guard with ->when() or return a sane default.*bold*, not **bold**; _italic_; no headings or tables. Escape user-supplied content (& → &, < → <, > → >) or mentions and links will render weirdly.Retry-After. Batch digests and queue instead of looping notify() inside a foreach.$this. A SlackMessage stored on the notification instance accumulates state across sends — build it fresh inside toSlack() or clone before appending.unfurlLinks/unfurlMedia were broken for a while and fixed in v3.1.0; if you're pinned below that, unfurling may be ignored. Also note unfurls apply to text, not to arbitrary block content.env() calls only work inside config/*.php; after changing SLACK_* variables run php artisan config:cache (and config:clear locally) or you'll chase a stale token.slack channel until after the Notification facade resolves, so a custom channel registered in your own provider can be overwritten depending on boot order. Register after resolution (or via Notification::resolved(...)) if you're aliasing slack.error()/success()/warning().Notification::sendNow()), dump $message->toArray(), then replay the same JSON with curl against the webhook or chat.postMessage to isolate app vs. Slack config.How can I help you explore Laravel packages today?