Building a Minimal Webhook Receiver with netcat in Bash
AI generated
$_
#!/
Bash · Networking · Webhooks · Debugging
Minimal Webhook Receiver with netcat
Accepting HTTP requests without a framework

For quickly debugging an incoming webhook locally, you do not need a full HTTP server. A minimal webhook receiver with netcat and a Bash loop accepts requests, shows headers and body, and answers with a valid HTTP response, in a few lines and with no dependencies.

16 min read nc · while loop · systemd Bash 4.x/5.x · Linux · Debugging

1. Why a webhook receiver with netcat makes sense

Before a webhook gets integrated into an application in production, you first need to clarify what the sender actually transmits: which headers, which content type, which JSON schema, which signature. A full-blown web server with routing and middleware is overkill for this phase. A minimal webhook receiver with netcat answers exactly this question in seconds, without setting up a project or opening a port in an existing application.

netcat, or nc for short, is available on practically every Linux system and opens a TCP listener on any port with a single call. Combined with a Bash loop, that becomes a webhook receiver with netcat that shows every incoming request in full, including all the headers many webhook providers use for signature verification. That is especially valuable when a third-party provider's documentation is incomplete and you need to know what actually comes over the wire.

Combined with a tunneling service like ngrok or cloudflared, such a receiver is also useful: the tunnel forwards the public webhook request to the local netcat listener, and you see in real time what an external service actually sends, before writing a single line of application code.

2. Netcat basics: nc -l, ports and variants

The base command for a webhook receiver with netcat is nc -l -p 8080 in the traditional variant (Debian/Ubuntu, package netcat-traditional) or nc -l 8080 in the OpenBSD variant, which is the default on many other distributions and macOS. The difference in option syntax is one of the most common pitfalls: -p for the port exists only in the traditional variant, while OpenBSD nc expects the port as a positional argument.

A single nc -l call accepts exactly one connection and then exits. For a persistent webhook receiver with netcat, you therefore need an infinite loop that restarts nc after every connection. The option -k (keep listening) exists in some nc variants but behaves inconsistently across implementations, which is why an explicit Bash loop is the more portable and more predictable solution.


#!/usr/bin/env bash
# detect-netcat.sh — identify which netcat variant is installed
set -euo pipefail

if nc -h 2>&1 | grep -q "\-\-version"; then
  echo "[INFO] Likely OpenBSD netcat: use 'nc -l PORT'"
elif nc -h 2>&1 | grep -q "\-p port"; then
  echo "[INFO] Likely traditional netcat: use 'nc -l -p PORT'"
else
  echo "[WARN] Could not reliably detect nc variant, test both syntaxes"
fi

nc -h 2>&1 | head -n 3

3. Building a simple HTTP receiver with a while loop

A webhook receiver with netcat needs two building blocks: a listener that accepts requests, and a fixed HTTP response that gets written back as soon as the request has arrived. The simplest form reads the entire request into a file, returns a minimal 200 OK response, and restarts the listener afterward in a loop.

The order matters: nc must write the response before the connection closes, otherwise the webhook sender waits for a reply that never comes and reports a failure after its own timeout. With tee, the incoming request can be logged and evaluated for the response logic at the same time.


#!/usr/bin/env bash
# webhook-receiver.sh — minimal webhook receiver built on netcat
set -euo pipefail

PORT="${1:-8080}"
LOG_DIR="/tmp/webhook-logs"
mkdir -p "$LOG_DIR"

echo "[INFO] Listening for webhooks on port $PORT (Ctrl+C to stop)"

while true; do
  request_file="$LOG_DIR/request-$(date +%s%N).log"

  # Capture the raw request, then send a fixed 200 OK response
  {
    printf 'HTTP/1.1 200 OK\r\n'
    printf 'Content-Type: text/plain\r\n'
    printf 'Content-Length: 2\r\n'
    printf 'Connection: close\r\n'
    printf '\r\n'
    printf 'OK'
  } | nc -l -p "$PORT" -q 1 > "$request_file" 2>/dev/null || true

  echo "[INFO] Request captured: $request_file"
  head -n 5 "$request_file"
  echo "---"
done

This script is deliberately minimal: a webhook receiver with netcat for debugging purposes does not need any real routing logic, it is enough to make the request visible. The option -q 1 tells nc to close the connection one second after input ends, which prevents the process from waiting forever for more data.

4. Writing back correct HTTP response headers

Many webhook senders check the HTTP status code of the response and retry on anything other than 2xx. A webhook receiver with netcat that only writes back raw data without valid HTTP headers is therefore interpreted by the sender as a failure, even if the request was received correctly. The response must begin with the status line, followed by the headers, a blank line, and optionally a body, each ending with CRLF (\r\n), not plain \n.

The Content-Length header must exactly match the byte length of the response body, otherwise some HTTP clients wait for more bytes until their own timeout kicks in. Anyone who wants to test how a webhook provider reacts to specific status codes can simply parameterize this value in the script and deliberately try out different responses (200, 400, 500).


#!/usr/bin/env bash
# webhook-receiver-status.sh — reply with a configurable HTTP status
set -euo pipefail

PORT="${1:-8080}"
STATUS="${2:-200 OK}"
BODY="${3:-{"received":true}}"
BODY_LENGTH=${#BODY}

response=$(printf 'HTTP/1.1 %s\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s' "$STATUS" "$BODY_LENGTH" "$BODY")

echo "[INFO] Replying with: HTTP/1.1 $STATUS"
printf '%s' "$response" | nc -l -p "$PORT" -q 1

5. Reading Content-Length and extracting the payload

A received webhook request consists of a status line, headers, a blank line, and the actual body, usually JSON. To cleanly extract the body from the recorded file, you find the first blank line and print everything after it. In Bash, awk handles that reliably in a single statement, regardless of how many headers the sender sends along.

The Content-Length header specifies how many bytes follow in the body. For a webhook receiver with netcat this matters because TCP has no concept of message boundaries: without Content-Length, you cannot be sure whether the full body has already arrived or the client is still sending more data. For debugging purposes, it is usually enough to log the value and compare it to the actual body length.


#!/usr/bin/env bash
# parse-webhook.sh — extract headers, content-length, and JSON body
set -euo pipefail

REQUEST_FILE="${1:?Usage: parse-webhook.sh <request-file>}"

# Everything before the first blank line is headers
headers=$(awk '/^\r?$/{exit} {print}' "$REQUEST_FILE")
content_length=$(echo "$headers" | grep -i '^Content-Length:' | awk '{print $2}' | tr -d '\r')

# Everything after the first blank line is the body
body=$(awk 'BEGIN{found=0} /^\r?$/{found=1; next} found{print}' "$REQUEST_FILE")

echo "[INFO] Declared Content-Length: ${content_length:-unknown}"
echo "[INFO] Actual body length: ${#body}"

if command -v jq &>/dev/null && [[ -n "$body" ]]; then
  echo "$body" | jq . 2>/dev/null || echo "[WARN] Body is not valid JSON"
fi

6. A persistent receiver with systemd instead of a one-off run

For occasional debugging, a script you start manually in the terminal is enough. But if a webhook receiver with netcat needs to run on a test or staging server for several days, a systemd service is the more robust solution: it restarts automatically after a crash, runs in the background, and writes logs via journalctl instead of into a loose text file.

The unit file simply calls the receiver script with Restart=always. That prevents a single faulty connection that crashes nc from permanently disabling the whole debugging receiver. For more production-like purposes, this approach should still remain only temporary, see section seven on security limits.

Tool Setup Effort HTTP Compliance Suited For
netcat + while loop Minimal, no setup Only what you write yourself Quick look at raw payloads
socat Low Somewhat more robust than nc Several simultaneous connections
Python http.server Medium Fully compliant More realistic application behavior
ngrok + local app Higher Fully compliant End-to-end test with a real application

7. Security: why netcat is never production ready

A webhook receiver with netcat is a debugging tool, not a production service. It lacks any signature verification, TLS termination, rate limiting, and error handling for parallel connections. A single nc process, in its simplest form, can only ever handle one connection at a time, which leads to lost requests when several webhooks arrive simultaneously, without the sender noticing.

More importantly: without checking the signature that most webhook providers send along in a header (such as X-Hub-Signature-256 for GitHub or Stripe-Signature for Stripe), such a receiver accepts any request, including forged ones. For local debugging that does not matter, but for any production use, a webhook receiver with netcat is fundamentally unsuitable and should never be reachable beyond your own developer network.

Mironsoft

Shell automation, webhook integrations and deployment infrastructure

Reliably receiving and processing webhooks?

We build production-ready webhook receivers with signature verification, retry logic and monitoring, and help debug third-party integrations locally for your Magento store.

Debugging Tools

Minimal receivers for quick analysis of incoming webhook payloads

Production Integration

Signature verification, idempotency and error handling for real webhook endpoints

Monitoring

Alerting on failed or missing webhooks

8. Debugging incoming webhooks from GitHub, Stripe and GitLab locally

The three most common webhook sources in a developer's daily work are GitHub (push and pull request events), GitLab (pipeline and merge request events), and Stripe (payment events). All three send structured JSON with a signature in a header. A webhook receiver with netcat, combined with a tunneling tool like ngrok, shows exactly which fields actually appear in the payload, often differing from outdated documentation examples.

This is especially helpful with Stripe, where event schemas can differ between API versions. Instead of developing against the production API, you configure the webhook to point, for testing, at the local ngrok URL that reaches the netcat receiver, and see every sent event with its full body in real time. That significantly shortens integration time because you do not have to jump back and forth between dashboard, logs, and code.

9. netcat vs. socat vs. a mini server compared

For simply looking at raw data, netcat remains the fastest option because no additional package needs to be installed. As soon as multiple connections need to arrive simultaneously, for example in load tests with several simulated webhook senders, socat with its more flexible fork options is the better choice, because it processes connections in parallel rather than one after another.

A small Python server with http.server or a tiny Node application delivers full HTTP compliance including correct chunked encoding and HTTP/1.1 keep-alive, which a webhook receiver with netcat does not offer. For the very first visual check of an unknown payload, netcat nonetheless remains the most pragmatic entry point, since no additional code needs to be written.

10. Summary

A minimal webhook receiver with netcat solves a very concrete problem: quickly seeing, without a framework, what a webhook sender actually sends. A while loop around nc -l accepts requests, a fixed or parameterized HTTP response with correct status code, Content-Length, and CRLF line endings keeps the sender happy, and awk cleanly extracts headers and body from the recorded file.

For ongoing debugging over several days, a systemd service makes the receiver robust against crashes. The limit remains important: a webhook receiver with netcat never belongs in production, because signature verification, TLS, and parallel processing are missing. For quick local analysis and for debugging third-party integrations, it is nonetheless one of the most useful Bash tools in everyday networking work.

Minimal Webhook Receiver with netcat — The Essentials at a Glance

Listener

nc -l -p PORT in a while loop, so the receiver keeps running after every request.

HTTP Response

Status line, headers, blank line, body, each with CRLF, plus an exact Content-Length.

Payload Parsing

awk splits headers and body at the first blank line, jq validates JSON.

Limits

No production use: no signature verification, no TLS, no real parallel processing.

11. FAQ: Minimal Webhook Receiver with netcat

1Suitable for production?
No. No signature verification, no TLS, no real parallel processing. Local debugging only.
2Which nc command is universal?
None. OpenBSD nc and traditional nc differ. Check with nc -h beforehand and test both syntaxes.
3Why timeout at the sender?
nc does not close the connection. With -q 1 it closes automatically one second after output ends.
4Extracting the body?
Use awk to print everything after the first blank line, which separates headers from the body per HTTP spec.
5Does Content-Length need to be exact?
Yes, otherwise some clients wait for more bytes until their own timeout.
6Test GitHub/Stripe webhooks locally?
Forward the public webhook to the local netcat listener with ngrok, see the real payload live.
7Multiple requests at once?
In simple form no, one connection at a time. Use socat with fork for parallelism.
8Keep it running permanently?
A while-true loop for short sessions or a systemd service with Restart=always for longer debugging.
9Difference to socat?
socat offers fork for parallelism and more complex bridges, but is less often preinstalled.
10Just use a Python server instead?
For full HTTP compliance yes. For a quick first check, netcat remains the most pragmatic entry point.