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.
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
-
Validate webhook URLs: Check that URLs match Discord’s webhook pattern before sending requests.
-
Use environment variables: Store webhook URLs in
.envfiles, never commit them to version control. -
Implement rate limiting: Discord allows 30 requests per minute per webhook. Queue messages if needed.
-
Sanitize user input: Always escape user-provided content to prevent injection attacks.
-
Log failures: Use proper logging to debug webhook issues in production.
-
Handle timeouts: Set reasonable timeout values (5-10 seconds) to prevent hanging requests.
-
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.
Related Articles
- Discord Webhook Embed Guide
- Discord Webhook Colors Reference
- Discord Webhook Notifications Best Practices
- Send Discord Webhooks with Python
- Discord Webhook Scheduled Messages — Automate recurring messages and timed notifications with scheduled webhooks
- Interactive Buttons and Actions — Add clickable buttons and action rows to your Discord webhook messages
Try it in our tool
Open Discord Webhook Builder