Forward Webhooks to Localhost Using Server-Sent Events
Published on December 28, 2025
WebhookApp now supports real-time webhook forwarding using Server-Sent Events (SSE). This allows you to stream incoming webhooks directly to your local development server.
How It Works
When a webhook arrives at your WebhookApp URL, it's instantly streamed to any connected SSE client. You can then forward it to your local development server.
External Service → WebhookApp → SSE Stream → Your Script → localhost:8080
The SSE Endpoint
Every webhook URL has a corresponding stream endpoint:
- Webhook URL:
https://webhookapp.dev/webhook/{uuid} - Stream URL:
https://webhookapp.dev/stream/{uuid}
Connect to the stream endpoint to receive webhooks in real-time.
Quick Start with curl
The simplest way to test the stream:
curl -N "https://webhookapp.dev/stream/YOUR-UUID-HERE"
You'll see events like:
event: connected
data: {"status":"connected","channel":"your-uuid"}
event: webhook
data: {"method":"POST","headers":{...},"content":"{\"order_id\":123}"}
event: ping
data: keep-alive
Forwarding Script (Bash)
Create a simple script to forward webhooks to localhost:
#!/bin/bash
# forward-webhooks.sh
UUID="$1"
TARGET="${2:-http://localhost:8080}"
echo "Forwarding webhooks to $TARGET..."
curl -N -s "https://webhookapp.dev/stream/$UUID" | while read line; do
if [[ $line == data:* ]]; then
payload="${line#data: }"
# Skip ping messages
if [[ $payload == "keep-alive" ]]; then
echo "♥ ping"
continue
fi
# Forward to local server
echo "$payload" | curl -s -X POST "$TARGET" \
-H "Content-Type: application/json" \
-d @- > /dev/null
echo "→ Forwarded to $TARGET"
fi
done
Usage:
chmod +x forward-webhooks.sh
./forward-webhooks.sh abc-123-uuid http://localhost:3000/webhook
Forwarding Script (PHP)
For more control, use this PHP script:
#!/usr/bin/env php
<?php
$uuid = $argv[1] ?? die("Usage: php forward.php <uuid> [target]\n");
$target = $argv[2] ?? 'http://localhost:8080';
$stream = fopen("https://webhookapp.dev/stream/{$uuid}", 'r');
if (!$stream) die("Failed to connect\n");
echo "🎯 Forwarding to: {$target}\n\n";
while (!feof($stream)) {
$line = fgets($stream);
if (strpos($line, 'data: ') === 0) {
$payload = trim(substr($line, 6));
if ($payload === 'keep-alive') {
echo "💓 ping\n";
continue;
}
$ch = curl_init($target);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
echo "✓ Forwarded → {$code}\n";
}
}
Forwarding Script (Node.js)
const https = require('https');
const http = require('http');
const uuid = process.argv[2];
const target = process.argv[3] || 'http://localhost:8080';
console.log(`🎯 Forwarding to: ${target}\n`);
https.get(`https://webhookapp.dev/stream/${uuid}`, (res) => {
res.on('data', (chunk) => {
const lines = chunk.toString().split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const payload = line.slice(6);
if (payload === 'keep-alive') {
console.log('💓 ping');
return;
}
const url = new URL(target);
const req = http.request({
hostname: url.hostname,
port: url.port || 80,
path: url.pathname,
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, (res) => {
console.log(`✓ Forwarded → ${res.statusCode}`);
});
req.write(payload);
req.end();
}
}
});
});
Event Types
The SSE stream sends these event types:
| Event | Description |
|---|---|
connected |
Initial connection confirmation |
webhook |
Incoming webhook data (JSON) |
ping |
Keep-alive signal (every 15 seconds) |
timeout |
Connection timeout (after 1 hour) |
Webhook Data Structure
Each webhook event contains:
{
"id": "request-uuid",
"method": "POST",
"headers": {"content-type": ["application/json"]},
"content": "{\"raw\":\"body\"}",
"query": {"param": "value"},
"request": {},
"ip": "203.0.113.50",
"json": {"parsed": "json"},
"timestamp": 1703779200
}
Tips
- Keep the connection alive - The stream times out after 1 hour. Reconnect if needed.
- Handle ping events - Ignore
keep-alivemessages in your forwarding logic. - Parse the JSON - The
jsonfield contains pre-parsed JSON when the body is valid JSON. - Check the method - Forward using the original HTTP method if your endpoint expects it.