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 Alerts Laravel Package

spatie/laravel-discord-alerts

Quickly send alerts to Discord from your Laravel app using a simple facade. Messages are dispatched via a queued job, so your app keeps running even if Discord is down. Just set a webhook URL and start notifying yourself of noteworthy events.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up the webhook in three commands:

composer require spatie/laravel-discord-alerts
php artisan vendor:publish --tag="discord-alerts-config"

Create a Discord channel → Integrations → Webhooks → New Webhook, copy the URL, and put it in .env:

DISCORD_ALERT_WEBHOOK=https://discord.com/api/webhooks/XXXX/YYYY

If you already have a queue worker running, you are done — the package ships a service provider and the facade is auto-registered. In local development, set DISCORD_ALERT_QUEUE_CONNECTION=sync so alerts land in Discord immediately instead of sitting in a queue you're not running.

First use case: drop a one-liner where a human would want to know something happened.

use Spatie\DiscordAlerts\Facades\DiscordAlert;

// app/Http/Controllers/NewsletterController.php
public function store(StoreNewsletterRequest $request)
{
    $newsletter = Newsletter::create($request->validated());

    DiscordAlert::message("You have a new subscriber to the **{$newsletter->name}** newsletter!");

    return back();
}

The first place to look when something is off is the published config/discord-alerts.php — particularly webhook_urls, job, queue_connection and queue. The second place is failed_jobs, because everything is dispatched as a job.

Implementation Patterns

Treat the facade as fire-and-forget, not as a transaction step

DiscordAlert::message() dispatches a job. It returns nothing useful and never throws because Discord is unreachable. That means it is safe to call inside a request cycle, an event listener, a console command, or a finally block. It also means do not put it before code that must run — if you need the alert to be the last thing, call it last.

DB::transaction(function () use ($order) {
    $order->markAsPaid();
    $order->invoice()->create([...]);
});

DiscordAlert::message(":moneybag: Order `{$order->reference}` paid — €{$order->total}");

Prefer a small alert class over scattered facade calls

After a month you'll have the same message formatted five different ways. Centralise it:

namespace App\Support\Alerts;

use Spatie\DiscordAlerts\Facades\DiscordAlert;

class OpsAlerts
{
    public static function orderFailed(Order $order, Throwable $e): void
    {
        DiscordAlert::to('ops')
            ->withUsername('Billing Bot')
            ->message(
                ":rotating_light: **Order failed**\n".
                "Ref: `{$order->reference}`\n".
                "Customer: {$order->customer_email}\n".
                "Error: `".class_basename($e)."` — {$e->getMessage()}"
            );
    }
}

Note the \n — multi-line messages are supported (newline formatting was added in 1.2.1), and Discord renders **bold**, _italic_, `code`, and :emoji: codes.

Embeds for anything structured

Plain text is fine for chatter; embeds are what you want for metrics, deploys, and exceptions. Discord embeds make colour a hex string here (converted to decimal under the hood):

DiscordAlert::message('Deployment finished', [
    [
        'title' => 'Production deploy',
        'description' => "Branch: `main`\nCommit: `{$sha}`\nDuration: {$seconds}s",
        'color' => $success ? '#2ECC71' : '#E74C3C',
        'author' => ['name' => 'Deployer', 'url' => config('app.url')],
        'fields' => [
            ['name' => 'Environment', 'value' => app()->environment(), 'inline' => true],
            ['name' => 'By', 'value' => $user->name, 'inline' => true],
        ],
    ],
]);

Multiple embeds per message are supported — just watch Discord's aggregate limits (see Gotchas).

Route alerts to different channels by key

Define named webhooks once, then route by intent rather than pasting URLs everywhere:

// config/discord-alerts.php
'webhook_urls' => [
    'default'    => env('DISCORD_ALERT_WEBHOOK'),
    'ops'        => env('DISCORD_ALERT_WEBHOOK_OPS'),
    'marketing'  => env('DISCORD_ALERT_WEBHOOK_MARKETING'),
],
DiscordAlert::to('ops')->message('Disk at 91% on web-03');
DiscordAlert::to('marketing')->message(':tada: Campaign `spring` went live');

to() also accepts a raw URL, which is handy for tenant-specific webhooks stored in the database:

DiscordAlert::to($tenant->discord_webhook)->message("New lead for {$tenant->name}");

Attach reports, not paths to volatile files

// Generated in-memory — always safe with a queued worker
DiscordAlert::attachData($csv, 'subscribers.csv')->message('Subscribers of this month');

// On-disk file — read when the job runs, so the path must survive
DiscordAlert::attach(storage_path('app/reports/daily.csv'))->message('Daily report');
DiscordAlert::attach($path, 'last-month.csv')->attach($path2, 'this-month.csv')->message('Reports');

Rule of thumb: if the file is temporary, lives in /tmp, is written by a different host than the queue worker, or is cleaned up by a nightly job — use attachData(). Override the guessed MIME type with the third argument when Discord/mime_content_type guesses wrong: attachData($csv, 'subscribers.txt', 'text/csv').

Debounce noisy alerts with delays

// "Still failing after 5 minutes" style summaries
DiscordAlert::to('ops')->delayMinutes(5)->message("{$count} failed jobs in the last 5 minutes");
DiscordAlert::to('ops')->delayHours(1)->delayMinutes(10)->message('Hourly digest'); // 70 minutes

Delays are additive and implemented with delay() on the queued job, so they only mean anything with an async queue driver.

Extend the job for retries, timeouts, and backoff

The default job has no retry policy. Give Discord a second chance without touching vendor code:

namespace App\Jobs;

use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;

class ReliableDiscordAlertJob extends SendToDiscordChannelJob
{
    public int $tries = 3;
    public int $timeout = 10;

    public function backoff(): array
    {
        return [10, 60];
    }
}
// config/discord-alerts.php
'job' => App\Jobs\ReliableDiscordAlertJob::class,

Hook into Laravel's error handling

The highest-leverage integration is exception reporting — the thing you actually want at 3am:

// bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->report(function (Throwable $e) {
        DiscordAlert::to('ops')->message(
            ":boom: `".class_basename($e)."` in ".app()->environment()."\n".
            "{$e->getMessage()}\n".
            "`".Str::limit($e->getFile(), 80).":".$e->getLine()."`"
        );
    })->stop(); // or omit to keep default logging
});

Careful: wrap this in a check so you don't spam yourself during a loop of 10k failures. Throttle by exception class + message hash, or use RateLimiter.

Testing

You can't fake the facade's HTTP layer directly, but you can assert a job was queued — which is the actual contract:

use Illuminate\Support\Facades\Queue;
use Spatie\DiscordAlerts\Jobs\SendToDiscordChannelJob;

it('alerts discord on new subscriber', function () {
    Queue::fake();

    $this->post('/newsletters', ['name' => 'Weekly']);

    Queue::assertPushed(SendToDiscordChannelJob::class);
});

For an end-to-end smoke test locally, set DISCORD_ALERT_QUEUE_CONNECTION=sync and hit the endpoint — a real message should appear in the channel.

Gotchas and Tips

Nothing is sent synchronously, so nothing fails loudly. Errors surface in failed_jobs, not in your request. If an alert is genuinely important, add a failed() method to your custom job (see below) or monitor failed_jobs itself.

attach() is evaluated at job runtime, attachData() at dispatch time. The file path is resolved by the worker, which may be a different container/machine or run minutes later. Temporary files will silently vanish and you'll get a broken attachment (or a failed job). Use attachData() for anything ephemeral.

attachData() bloats your queue payload. The base64 content lives in the serialized job. With the database driver this means large rows in the jobs table; with Redis it means large values and possible maxmemory pressure. Keep payloads small, or write to a shared disk (S3/EBS) and use attach() with a guaranteed-life path.

Discord's hard limits will silently truncate or reject: message content ≤ 2000 characters, ≤ 10 embeds per message, ≤ 6000 characters total across all embeds, embed title ≤ 256, description ≤ 4096, ≤ 25 fields, ≤ 10 files and ≤ 8MB total per message (unboosted servers). Also ~5 requests per 5 seconds per webhook — a burst of alerts will get rate-limited, and the package does not retry for you by default. Batch or delay.

message() ends the chain. The builder is a pending-dispatch object: configure first (to(), withUsername(), withAvatar(), enableTTS(), delay*(), attach*()), then call message() last. Calling ->message()->delayMinutes(5) won't do what you want.

TTS takes a truthy string in the README (->enableTTS('true')) — note that 'false' is also truthy in PHP. Pass a real boolean (enableTTS(true)) to avoid surprises.

withAvatar() requires a key defined in avatar_urls. The default is an empty string, which the package deliberately omits from the payload. Add your URLs in config first; passing an unknown key will not magically resolve.

Named webhook vs URL detection. to('marketing') looks up config; to('https://...') is treated as a raw URL. A typo'd key that isn't a URL will produce an "unknown webhook" style failure — check the webhook_urls array keys before debugging further.

Config caching bites on env changes. webhook_urls reads env() at config-load time. After editing .env in a cached-config environment run php artisan config:clear (or config:cache in deploy), otherwise your new webhook is ignored.

Queue connection and queue name are separate concerns. DISCORD_ALERT_QUEUE_CONNECTION selects the driver connection; DISCORD_ALERT_QUEUE selects the queue name and is ignored for sync and null drivers. If your workers only consume named queues (e.g. --queue=high,default), make sure discord-alerts alerts land on a queue someone actually consumes — otherwise they'll queue forever and look "sent" in your code.

Delays don't work on sync. delayMinutes()/delayHours() call ->delay() on the job; the sync driver executes immediately. Expect this to differ between local and production.

Redact the webhook URL. It is a bearer credential — anyone with it can post to your channel. Keep it in .env (never in config/discord-alerts.php literals for named webhooks), exclude it from logs, and rotate it in Discord if it leaks. If you accept custom webhook URLs from users, validate they're discord.com/api/webhooks/ hosts to avoid SSRF.

Add a failed() hook for dead-letter visibility. A silent Discord failure is worse than no alert:

public function failed(Throwable $e): void
{
    Log::error('Discord alert failed', ['exception' => $e]);
}

Don't use this for user-facing notifications. There's no per-recipient delivery, no templating, no read state. For anything a customer should receive, use Laravel's notification system; this package is for you and your team. And for a Slack-shaped org, the sibling spatie/laravel-slack-alerts has the same API surface, so keeping your alert classes thin makes swapping trivial.

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.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin