Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Discord Webhook Channel Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.


Implementation Patterns

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.


Gotchas and Tips

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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource