Skip to main content
All articles
· Updated February 15, 2026

Send Discord Webhook Messages with PHP

Complete guide to sending Discord webhook messages using PHP. Learn cURL and Guzzle methods, embed creation, file uploads, and Laravel integration.

phpwebhooktutoriallaravelphp discord 2026laravel discordwordpress discord
Send Discord Webhook Messages with PHP

Discord webhooks provide a simple HTTP API for sending messages to Discord channels from PHP applications. Whether you’re building WordPress plugins, Laravel applications, or standalone scripts, PHP offers multiple approaches to integrate with Discord.

This guide covers everything from basic cURL requests to production-ready Laravel services with proper error handling and retry logic.

Prerequisites

You’ll need:

  • PHP 7.4 or later (8.0+ recommended)
  • cURL extension enabled (usually included by default)
  • A Discord webhook URL from your server’s channel settings
  • Optional: Composer for Guzzle HTTP client

Basic Webhook with PHP cURL

The simplest approach uses PHP’s built-in cURL functions:

<?php

function sendDiscordMessage($webhookUrl, $message) {
    $data = json_encode(['content' => $message]);
    
    $ch = curl_init($webhookUrl);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_POST, 1);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    return $httpCode >= 200 && $httpCode < 300;
}

// Usage
$webhookUrl = 'https://discord.com/api/webhooks/YOUR_WEBHOOK_URL';
sendDiscordMessage($webhookUrl, 'Hello from PHP!');

This sends a plain text message. For production use, you’ll want better error handling and support for rich embeds.

Using Guzzle HTTP Client

Guzzle provides a cleaner, more modern API. Install it via Composer:

composer require guzzlehttp/guzzle

Basic implementation:

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class DiscordWebhook {
    private $client;
    private $webhookUrl;
    
    public function __construct($webhookUrl) {
        $this->webhookUrl = $webhookUrl;
        $this->client = new Client([
            'timeout' => 10,
            'headers' => [
                'Content-Type' => 'application/json'
            ]
        ]);
    }
    
    public function sendMessage($content) {
        try {
            $response = $this->client->post($this->webhookUrl, [
                'json' => ['content' => $content]
            ]);
            
            return $response->getStatusCode() === 204;
        } catch (RequestException $e) {
            error_log('Discord webhook failed: ' . $e->getMessage());
            return false;
        }
    }
}

// Usage
$webhook = new DiscordWebhook('https://discord.com/api/webhooks/YOUR_WEBHOOK_URL');
$webhook->sendMessage('Hello from Guzzle!');

Guzzle automatically handles JSON encoding, headers, and provides better exception handling than raw cURL.

Creating Rich Embeds

Discord embeds let you send formatted messages with colors, fields, images, and more. Here’s a complete embed class:

<?php

class DiscordEmbed {
    private $data = [];
    
    public function setTitle($title) {
        $this->data['title'] = $title;
        return $this;
    }
    
    public function setDescription($description) {
        $this->data['description'] = $description;
        return $this;
    }
    
    public function setColor($color) {
        // Accept hex string or integer
        if (is_string($color)) {
            $color = hexdec(ltrim($color, '#'));
        }
        $this->data['color'] = $color;
        return $this;
    }
    
    public function addField($name, $value, $inline = false) {
        if (!isset($this->data['fields'])) {
            $this->data['fields'] = [];
        }
        
        $this->data['fields'][] = [
            'name' => $name,
            'value' => $value,
            'inline' => $inline
        ];
        return $this;
    }
    
    public function setFooter($text, $iconUrl = null) {
        $this->data['footer'] = ['text' => $text];
        if ($iconUrl) {
            $this->data['footer']['icon_url'] = $iconUrl;
        }
        return $this;
    }
    
    public function setThumbnail($url) {
        $this->data['thumbnail'] = ['url' => $url];
        return $this;
    }
    
    public function setImage($url) {
        $this->data['image'] = ['url' => $url];
        return $this;
    }
    
    public function setTimestamp($timestamp = null) {
        $this->data['timestamp'] = $timestamp ?? date('c');
        return $this;
    }
    
    public function toArray() {
        return $this->data;
    }
}

Update the webhook class to support embeds:

<?php

class DiscordWebhook {
    // ... previous code ...
    
    public function sendEmbed($content = null, $embeds = []) {
        $payload = [];
        
        if ($content) {
            $payload['content'] = $content;
        }
        
        if (!empty($embeds)) {
            $payload['embeds'] = array_map(function($embed) {
                return $embed instanceof DiscordEmbed ? $embed->toArray() : $embed;
            }, $embeds);
        }
        
        try {
            $response = $this->client->post($this->webhookUrl, [
                'json' => $payload
            ]);
            
            return $response->getStatusCode() === 204;
        } catch (RequestException $e) {
            error_log('Discord webhook failed: ' . $e->getMessage());
            return false;
        }
    }
}

Example usage with an order notification:

<?php

$embed = (new DiscordEmbed())
    ->setTitle('New Order Received')
    ->setDescription('Order #12345 has been placed')
    ->setColor('#00FF00')
    ->addField('Customer', 'John Doe', true)
    ->addField('Total', '$149.99', true)
    ->addField('Items', '3', true)
    ->addField('Products', "- Premium Widget\n- Deluxe Gadget\n- Standard Tool", false)
    ->setFooter('E-commerce System')
    ->setTimestamp();

$webhook = new DiscordWebhook('https://discord.com/api/webhooks/YOUR_WEBHOOK_URL');
$webhook->sendEmbed(null, [$embed]);

For more embed design patterns, check out our Discord embed formatting guide.

Sending File Attachments

To send files with your webhook, use multipart form data:

<?php

class DiscordWebhook {
    // ... previous code ...
    
    public function sendFile($content, $filePath) {
        if (!file_exists($filePath)) {
            throw new Exception("File not found: $filePath");
        }
        
        $payload = ['content' => $content];
        
        try {
            $response = $this->client->post($this->webhookUrl, [
                'multipart' => [
                    [
                        'name' => 'payload_json',
                        'contents' => json_encode($payload)
                    ],
                    [
                        'name' => 'file',
                        'contents' => fopen($filePath, 'r'),
                        'filename' => basename($filePath)
                    ]
                ]
            ]);
            
            return $response->getStatusCode() === 200;
        } catch (RequestException $e) {
            error_log('Discord file upload failed: ' . $e->getMessage());
            return false;
        }
    }
}

// Usage
$webhook->sendFile('Here is the report:', '/path/to/report.pdf');

Error Handling and Retry Logic

Production applications need proper error handling for network failures, rate limits, and invalid payloads:

<?php

class DiscordWebhook {
    // ... previous code ...
    
    public function sendWithRetry($content, $maxRetries = 3) {
        $attempt = 0;
        
        while ($attempt < $maxRetries) {
            try {
                $response = $this->client->post($this->webhookUrl, [
                    'json' => ['content' => $content]
                ]);
                
                if ($response->getStatusCode() === 204) {
                    return true;
                }
                
            } catch (RequestException $e) {
                $attempt++;
                
                // Handle rate limiting
                if ($e->hasResponse() && $e->getResponse()->getStatusCode() === 429) {
                    $retryAfter = $e->getResponse()->getHeader('Retry-After')[0] ?? 5;
                    error_log("Rate limited. Retrying after {$retryAfter}s");
                    sleep($retryAfter);
                    continue;
                }
                
                // Log error
                error_log("Discord webhook failed (attempt $attempt): " . $e->getMessage());
                
                // Exponential backoff
                if ($attempt < $maxRetries) {
                    sleep(pow(2, $attempt));
                }
            }
        }
        
        return false;
    }
}

This implementation:

  • Retries up to 3 times with exponential backoff
  • Respects Discord’s rate limit headers
  • Logs errors for debugging
  • Returns success/failure status

Laravel Integration

For Laravel applications, create a dedicated service class:

<?php

namespace App\Services;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Log;

class DiscordNotificationService {
    private $client;
    private $webhookUrl;
    
    public function __construct() {
        $this->webhookUrl = config('services.discord.webhook_url');
        $this->client = new Client([
            'timeout' => 10,
            'headers' => ['Content-Type' => 'application/json']
        ]);
    }
    
    public function notify($message, $embeds = []) {
        if (!$this->webhookUrl) {
            Log::warning('Discord webhook URL not configured');
            return false;
        }
        
        $payload = ['content' => $message];
        
        if (!empty($embeds)) {
            $payload['embeds'] = $embeds;
        }
        
        try {
            $response = $this->client->post($this->webhookUrl, [
                'json' => $payload
            ]);
            
            Log::info('Discord notification sent successfully');
            return true;
            
        } catch (RequestException $e) {
            Log::error('Discord notification failed', [
                'error' => $e->getMessage(),
                'payload' => $payload
            ]);
            return false;
        }
    }
    
    public function notifyError($exception, $context = []) {
        $embed = [
            'title' => 'Application Error',
            'description' => $exception->getMessage(),
            'color' => hexdec('FF0000'),
            'fields' => [
                [
                    'name' => 'File',
                    'value' => $exception->getFile(),
                    'inline' => false
                ],
                [
                    'name' => 'Line',
                    'value' => (string) $exception->getLine(),
                    'inline' => true
                ],
                [
                    'name' => 'Environment',
                    'value' => config('app.env'),
                    'inline' => true
                ]
            ],
            'timestamp' => now()->toIso8601String()
        ];
        
        if (!empty($context)) {
            $embed['fields'][] = [
                'name' => 'Context',
                'value' => json_encode($context, JSON_PRETTY_PRINT),
                'inline' => false
            ];
        }
        
        return $this->notify('@here Application error occurred!', [$embed]);
    }
}

Add to config/services.php:

'discord' => [
    'webhook_url' => env('DISCORD_WEBHOOK_URL'),
],

Register as a singleton in AppServiceProvider:

<?php

namespace App\Providers;

use App\Services\DiscordNotificationService;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider {
    public function register() {
        $this->app->singleton(DiscordNotificationService::class);
    }
}

Use in controllers or jobs:

<?php

namespace App\Http\Controllers;

use App\Services\DiscordNotificationService;

class OrderController extends Controller {
    public function store(Request $request, DiscordNotificationService $discord) {
        $order = Order::create($request->validated());
        
        $discord->notify("New order #{$order->id} received!");
        
        return response()->json($order, 201);
    }
}

Laravel Notification Channel

For a more Laravel-native approach, create a custom notification channel:

<?php

namespace App\Notifications\Channels;

use App\Services\DiscordNotificationService;
use Illuminate\Notifications\Notification;

class DiscordChannel {
    private $discord;
    
    public function __construct(DiscordNotificationService $discord) {
        $this->discord = $discord;
    }
    
    public function send($notifiable, Notification $notification) {
        $message = $notification->toDiscord($notifiable);
        
        return $this->discord->notify(
            $message['content'] ?? null,
            $message['embeds'] ?? []
        );
    }
}

Create a notification:

<?php

namespace App\Notifications;

use App\Notifications\Channels\DiscordChannel;
use Illuminate\Notifications\Notification;

class OrderPlaced extends Notification {
    private $order;
    
    public function __construct($order) {
        $this->order = $order;
    }
    
    public function via($notifiable) {
        return [DiscordChannel::class];
    }
    
    public function toDiscord($notifiable) {
        return [
            'embeds' => [[
                'title' => 'New Order',
                'description' => "Order #{$this->order->id}",
                'color' => hexdec('00FF00'),
                'fields' => [
                    ['name' => 'Customer', 'value' => $this->order->customer_name, 'inline' => true],
                    ['name' => 'Total', 'value' => '$' . $this->order->total, 'inline' => true]
                ]
            ]]
        ];
    }
}

Send the notification:

$order->notify(new OrderPlaced($order));

WordPress Plugin Integration

For WordPress plugins, hook into actions to send notifications:

<?php

class WP_Discord_Webhook {
    private $webhookUrl;
    
    public function __construct($webhookUrl) {
        $this->webhookUrl = $webhookUrl;
        
        // Hook into WordPress actions
        add_action('publish_post', [$this, 'onPostPublished'], 10, 2);
        add_action('wp_login', [$this, 'onUserLogin'], 10, 2);
        add_action('woocommerce_new_order', [$this, 'onNewOrder'], 10, 1);
    }
    
    public function onPostPublished($postId, $post) {
        $embed = [
            'title' => 'New Post Published',
            'description' => $post->post_title,
            'url' => get_permalink($postId),
            'color' => hexdec('0099FF'),
            'fields' => [
                ['name' => 'Author', 'value' => get_the_author_meta('display_name', $post->post_author), 'inline' => true],
                ['name' => 'Category', 'value' => $this->getCategories($postId), 'inline' => true]
            ],
            'timestamp' => date('c')
        ];
        
        $this->send(null, [$embed]);
    }
    
    public function onUserLogin($username, $user) {
        $this->send("User **{$username}** logged in");
    }
    
    public function onNewOrder($orderId) {
        $order = wc_get_order($orderId);
        
        $embed = [
            'title' => 'New WooCommerce Order',
            'description' => "Order #{$orderId}",
            'color' => hexdec('00FF00'),
            'fields' => [
                ['name' => 'Customer', 'value' => $order->get_billing_first_name() . ' ' . $order->get_billing_last_name(), 'inline' => true],
                ['name' => 'Total', 'value' => $order->get_formatted_order_total(), 'inline' => true],
                ['name' => 'Items', 'value' => (string) $order->get_item_count(), 'inline' => true]
            ]
        ];
        
        $this->send(null, [$embed]);
    }
    
    private function send($content, $embeds = []) {
        $payload = [];
        if ($content) $payload['content'] = $content;
        if ($embeds) $payload['embeds'] = $embeds;
        
        wp_remote_post($this->webhookUrl, [
            'headers' => ['Content-Type' => 'application/json'],
            'body' => json_encode($payload),
            'timeout' => 10
        ]);
    }
    
    private function getCategories($postId) {
        $categories = get_the_category($postId);
        return implode(', ', array_map(fn($cat) => $cat->name, $categories));
    }
}

// Initialize
$webhookUrl = get_option('discord_webhook_url');
if ($webhookUrl) {
    new WP_Discord_Webhook($webhookUrl);
}

Best Practices

  1. Validate webhook URLs: Check that URLs match Discord’s webhook pattern before sending requests.

  2. Use environment variables: Store webhook URLs in .env files, never commit them to version control.

  3. Implement rate limiting: Discord allows 30 requests per minute per webhook. Queue messages if needed.

  4. Sanitize user input: Always escape user-provided content to prevent injection attacks.

  5. Log failures: Use proper logging to debug webhook issues in production.

  6. Handle timeouts: Set reasonable timeout values (5-10 seconds) to prevent hanging requests.

  7. Test before deploying: Use our visual webhook builder to design and test messages before implementing them in code.

Testing Your Implementation

Quick test script:

<?php

require 'vendor/autoload.php';

$webhook = new DiscordWebhook($_ENV['DISCORD_WEBHOOK_URL']);

// Test 1: Simple message
echo "Test 1: Simple message... ";
echo $webhook->sendMessage('Test message') ? "✓\n" : "✗\n";

// Test 2: Embed
echo "Test 2: Embed... ";
$embed = (new DiscordEmbed())
    ->setTitle('Test Embed')
    ->setDescription('This is a test')
    ->setColor('#FF0000');
echo $webhook->sendEmbed(null, [$embed]) ? "✓\n" : "✗\n";

// Test 3: Retry logic
echo "Test 3: Retry logic... ";
echo $webhook->sendWithRetry('Test retry') ? "✓\n" : "✗\n";

Next Steps

You now have a complete foundation for sending Discord webhooks from PHP. Consider:

  • Building reusable embed templates for different event types
  • Creating a centralized notification service for your application
  • Integrating webhooks into your CI/CD pipeline
  • Adding message queuing for high-volume scenarios

The patterns shown here work for everything from simple scripts to large Laravel applications. Start with what you need, then expand as your requirements grow.

discord-webhook.com also offers scheduled messages, thread and forum support, polls, and interactive buttons with actions — all configurable through the visual builder without writing code.