snoeren-development/laravel-discord-webhook-channel
A Laravel notification channel for sending messages to Discord through webhooks. Configure a Discord webhook URL, add the channel to your notifications, and deliver formatted messages to your server with minimal setup.
1. Install the package
composer require snoeren-development/laravel-discord-webhook-channel
2. Publish the config
php artisan vendor:publish --tag="discord-webhook-channel-config"
The published config/discord-webhook-channel.php holds the default webhook_url (and usually username / avatar_url). Add it to .env:
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/1234567890/abcdef...
Treat that URL like a password — anyone with it can post to the channel.
3. Send your first notification
php artisan make:notification NewSignupNotification
use SnoerenDevelopment\DiscordWebhookChannel\DiscordWebhookChannel;
use SnoerenDevelopment\DiscordWebhookChannel\DiscordWebhookMessage;
public function via(object $notifiable): array
{
return [DiscordWebhookChannel::class];
}
public function toDiscordWebhook(object $notifiable): DiscordWebhookMessage
{
return DiscordWebhookMessage::create()
->content("🚀 New signup: {$notifiable->email}");
}
4. Trigger it
$user->notify(new NewSignupNotification());
First use case to reach for: operational alerts — failed jobs, deploy notifications, new signups, Stripe failures. It is the "fire a message into a team channel" package, not a chat bot or a two-way integration.
Where to look first: the package is small (14 stars, MIT). Skip guesswork and read src/DiscordWebhookMessage.php — it is the single source of truth for which Discord payload fields are exposed, and it changes between releases.
Rich embeds for anything that isn't a one-liner
Plain content() is fine for chat noise, but structured embeds render far better for alerts and are rate-limit friendly (fewer messages per event):
return DiscordWebhookMessage::create()
->username('Deploy Bot')
->embed(
title: 'Deployment succeeded',
description: "Commit `{$sha}` shipped to production.",
color: 0x57F287, // decimal, see Gotchas
);
Most versions of this package also expose success(), error(), warning(), and info() helpers that wrap an embed with the standard Discord color palette — prefer these over hand-picking hex colors.
Per-notifiable routing (multi-tenant / multi-channel)
Add a route method to the notifiable model when different models should hit different channels:
public function routeNotificationForDiscordWebhook(): string
{
return $this->discord_webhook_url;
}
The channel resolves this before falling back to config('discord-webhook-channel.webhook_url'). This lets you store a webhook URL per team/workspace and post into the right channel automatically.
On-demand notifications without a model
For cron jobs, one-off scripts, or admin tooling, use the facade and skip the model entirely:
Notification::route(DiscordWebhookChannel::class, $webhookUrl)
->notify(new DeployFinishedNotification());
This is also the easiest way to post from a console command or a queue job.
Always queue the notification
The channel makes a synchronous HTTP call. Wrap the notification in ShouldQueue so a Discord outage never blocks a user-facing request:
class DeployFinishedNotification extends Notification implements ShouldQueue
{
public int $tries = 3;
public array $backoff = [10, 60, 300];
}
Discord rate-limits per webhook (roughly 5 messages per 2 seconds per webhook in practice, plus global limits). Backoff on 429 is the difference between a working alert pipeline and one that silently drops messages at 3am.
Gate alerts behind config
Operational channels shouldn't fire on every local composer test. Guard in via():
public function via(object $notifiable): array
{
return config('services.discord.alerts_enabled')
? [DiscordWebhookChannel::class]
: [];
}
Testing
Notification::fake() asserts that a notification was sent but not the payload — it never touches the channel's HTTP layer. Two complementary strategies:
// 1. Assert the notification was dispatched
Notification::fake();
$user->notify(new NewSignupNotification($user));
Notification::assertSentTo($user, NewSignupNotification::class);
// 2. Assert the actual Discord payload
Http::fake(['discord.com/*' => Http::response(null, 204)]);
$user->notify(new NewSignupNotification($user));
Http::assertSent(fn ($req) =>
$req['content'] === "🚀 New signup: {$user->email}"
);
Use (1) in feature tests for notification flow and (2) in tests that verify message formatting.
Empty messages are a 400
Discord rejects payloads with no content and no embeds. A conditional that sometimes produces an empty message will fail at runtime, not at build time — guard it:
if ($this->lines->isEmpty()) {
return; // or send a placeholder
}
Colors are integers, not hex strings
Discord wants a decimal color field. 0x57F287 is fine in PHP but the string "#57F287" will be rejected. Convert with hexdec('#57F287') if you're pulling from config or a design token.
Hard limits to design around
| Field | Limit |
|---|---|
content |
2000 characters |
username |
80 characters |
| Embed title | 256 characters |
| Embed description | 4096 characters |
| Fields per embed | 25 (name 256, value 1024) |
| Embeds per message | 10 |
Discord truncates nothing — it returns 400 for oversize payloads. Truncate content and embed fields yourself with Str::limit() when the source is user-generated.
Markdown is limited
Discord supports **bold**, *italic*, `code`, ```code block```, and > quote. It does not support tables, headings (#), or <img> tags. Links must be bare URLs or inside embeds (title + url). Test with the actual client before assuming formatting renders.
Mentions need allow-listing
<@123456>, <@&789>, @everyone, @here all work in content, but many teams lock down @everyone on the webhook. If you're building a "notify the on-call" flow, prefer a role mention (<@&ROLE_ID>) and verify the webhook has that permission in Server Settings → Integrations.
Config caching bites hard
After editing config/discord-webhook-channel.php or DISCORD_WEBHOOK_URL:
php artisan config:clear
# or, on deploy:
php artisan config:cache
A stale config cache silently sends to the old webhook — a very confusing 30 minutes.
Reading the response code
| Code | Meaning |
|---|---|
204 |
Success (empty body — this is normal) |
400 |
Malformed payload (empty message, oversize field, bad color) |
401 |
Invalid webhook token |
404 |
Webhook deleted from Discord |
429 |
Rate limited — inspect X-RateLimit-Reset-After |
The package does not throw on non-2xx by default in most versions; if you need alerting-on-alert-failure, wrap it or check the config for a throw flag.
Posting into threads
Append the thread to the URL rather than switching channels:
https://discord.com/api/webhooks/{id}/{token}?thread_id={thread_id}
The first ? becomes & if the webhook URL already has query parameters — concatenating blindly produces a broken URL and a 404.
TTS is essentially dead
The tts() field still validates but modern Discord clients ignore it for webhook messages. Don't design user-facing flows around it.
Extension points are thin
The channel is a single class; the message builder is a fluent DTO. If you need embeds, buttons, or components that aren't covered, subclass DiscordWebhookMessage and override the array serialization, or bypass the package and post via Http::post($webhook, $payload) — for a 14-star package, that is often less work than working against the abstraction.
Version drift is real
Last release was 2026-03-30. Because the package is small and the Discord webhook API is stable but not versioned, check CHANGELOG when bumping — the message builder's method signatures (especially embed() / embeds()) have shifted across versions. Lock to a minor version in composer.json if you're deploying frequently.
How can I help you explore Laravel packages today?