A webhook payload example you can send and inspect

Send a synthetic JSON webhook to a temporary local Python receiver, inspect its headers and body, and compare a successful response with invalid input.

Beginner6 min read
In this guide

A webhook payload is the body of a request sent to tell another system that something happened. For example, a service might send a JSON object when a record is created. The receiving system reads the request and sends back a response.

Here you will send a small synthetic payload, see exactly what arrives, and read the receiver’s answer. One Python script runs both sides on your computer and closes the receiver afterward. You do not need a SaaS account, a public URL, or paid hosting.

The commands below use an Ubuntu terminal, including Ubuntu in WSL, with Python 3.12 already available. You need to be able to save a plain-text file and run a terminal command. Use only the fictional data below: this example prints the entire request body.

The receiver binds to 127.0.0.1, the address for this computer, on a temporary port. Keep that address unchanged. This is a local learning example, not an endpoint for an outside service; Python’s http.server is not intended as a production web server.

Create the payload

Open your Ubuntu terminal and check Python:

python3 --version

Continue if it reports Python 3.12.x. If python3 is missing or you are in a different environment, stop here and use a Python 3.12 environment before running the example. No packages or pip installation are needed.

Create a fresh folder and enter it:

mkdir webhook-payload-lab && cd webhook-payload-lab

If that folder already exists, use a new empty folder rather than overwriting previous work. In your text editor, save this as payload.json inside the folder:

{
  "event_id": "evt_local_001",
  "event_type": "example.created",
  "data": {
    "item_id": "demo_001",
    "quantity": 2
  }
}

JSON represents named fields and values. Field names and text values use double quotes; quantity is a number, so it has no quotes. data contains another object, enclosed in braces.

These names belong to this example. event_id identifies the fictional event, event_type describes it, and data holds its details. Real senders define their own fields and may put event identifiers in headers instead. Use their documentation when connecting a real integration.

Save the local sender and receiver

In the same folder, save this complete file as inspect_webhook.py. Its first part receives and validates the request. Its last part sends payload.json to that receiver and prints the answer.

import http.client
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from threading import Thread


class Receiver(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        pass

    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", "0")))
        print(f"Received: POST {self.path}")
        print(f"Content-Type: {self.headers.get('Content-Type')}")
        print("Payload:")
        print(raw.decode("utf-8", errors="replace"))
        try:
            payload = json.loads(raw)
        except (ValueError, UnicodeDecodeError):
            status, answer = 400, {"error": "Body must be valid JSON"}
        else:
            valid = (
                isinstance(payload, dict)
                and isinstance(payload.get("event_id"), str)
                and bool(payload["event_id"])
                and isinstance(payload.get("event_type"), str)
                and bool(payload["event_type"])
                and isinstance(payload.get("data"), dict)
            )
            if valid:
                status = 200
                answer = {"received": True, "event_id": payload["event_id"]}
            else:
                status = 422
                answer = {"error": "Need event_id, event_type, and a data object"}
        body = json.dumps(answer).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)


if len(sys.argv) != 2:
    raise SystemExit("Usage: python3 inspect_webhook.py payload.json")
try:
    raw = Path(sys.argv[1]).read_bytes()
except OSError as error:
    raise SystemExit(f"Cannot read payload file: {error}")

with HTTPServer(("127.0.0.1", 0), Receiver) as server:
    worker = Thread(target=server.serve_forever, daemon=True)
    worker.start()
    client = http.client.HTTPConnection(*server.server_address, timeout=3)
    try:
        client.request("POST", "/webhook", body=raw,
                       headers={"Content-Type": "application/json"})
        response = client.getresponse()
        print(f"Response: {response.status} {response.reason}")
        print(response.read().decode("utf-8"))
    finally:
        client.close()
        server.shutdown()
        worker.join()
print("Receiver stopped.")

Return to the terminal that is inside webhook-payload-lab and run:

python3 inspect_webhook.py payload.json

If Python says it cannot open the script, check that both files were saved in this folder with the exact names above, without an added .txt extension. Cannot read payload file means the script was found but the payload path needs correcting. Fix the filename or location and run the same command again.

Read the request and response

The output begins with Received: POST /webhook and Content-Type: application/json, followed by your JSON. POST is the request method used to send the body. /webhook is the destination path in this example. The content-type header tells the receiver what format the sender claims the body contains; it does not make an invalid body valid.

The final lines should be:

Response: 200 OK
{"received": true, "event_id": "evt_local_001"}
Receiver stopped.

Compare the returned event_id with the one in your file. Matching values show that this receiver parsed this request and constructed an answer from it. The payload contains the fictional quantity 2, and the printed body lets you inspect it without guessing which fields were sent.

Here, 200 means the receiver accepted the example’s required structure. It does not mean a spreadsheet row was created or a payment was processed: this receiver performs no such work.

Try one bad input, then stop

Save another file named invalid.json in the same folder containing this single line:

{}

Send it with:

python3 inspect_webhook.py invalid.json

You should receive 422 Unprocessable Entity and an error explaining the missing fields. {} is valid JSON, but it does not have the structure this receiver requires. An entirely empty file or broken JSON instead produces 400 Bad Request and Body must be valid JSON. Restore the original payload by running the command with payload.json again; there is no server configuration to reset.

Each run ends with Receiver stopped. and returns you to the terminal prompt. Nothing stays running in the background. If you need to interrupt a run, press Ctrl+C in that terminal. When finished, you can delete the lab folder through your file manager; it contains only the files you created.

For a real webhook, the next step is to read the sender’s event schema, authentication or signature requirements, and delivery/retry rules. This example does not verify a sender or remember event IDs to prevent duplicate processing. Do not expose it to the internet or send real account data to it.

← Back to automation guides