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

  1. Keep the connection alive - The stream times out after 1 hour. Reconnect if needed.
  2. Handle ping events - Ignore keep-alive messages in your forwarding logic.
  3. Parse the JSON - The json field contains pre-parsed JSON when the body is valid JSON.
  4. Check the method - Forward using the original HTTP method if your endpoint expects it.