hokio.

HOKIO / DEVELOPER GUIDES

Receive webhooks on localhost with Python.

A small, dependency-free receiver for testing an AI callback, automation event, or webhook integration with Hokio. The example accepts JSON at POST /webhook and returns a receipt.

1. Run a local webhook receiver

You need Python 3.11 or newer for this example. Download webhook_receiver.py into an empty directory, review the file, and run it:

python webhook_receiver.py

On Windows, use py -3 webhook_receiver.py if your Python installation uses the py launcher. On Linux, use python3 webhook_receiver.py.

Expected startup message: Listening on http://127.0.0.1:8000/webhook. Leave this terminal open. The receiver binds to loopback; it does not open a listener on your public network interface.

What the receiver does

  • Accepts POST /webhook with a JSON object up to 64 KiB.
  • Returns the event name and a JSON receipt, without printing payloads or credentials.
  • Provides GET /health to check the local process.
  • Returns an error for invalid JSON, oversized bodies, or unsupported paths.

The essential request flow is:

External service → your Hokio HTTPS URL → Hokio connector
                 → http://127.0.0.1:8000/webhook

This is a development receiver. Before using real customer events, verify your provider's webhook signature, authenticate requests, and implement duplicate-event handling in your application. The example intentionally does not perform business actions.

Read the complete Python source
"""Development-only JSON webhook receiver. Run with Python 3.11+.

No business actions, persistent storage, or payload logging. Add provider signature
verification and duplicate-event handling before using real webhook data.
"""
from __future__ import annotations

import argparse
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any

MAX_BODY = 64 * 1024


class WebhookHandler(BaseHTTPRequestHandler):
    def setup(self) -> None:
        super().setup()
        self.connection.settimeout(10)

    def log_message(self, format: str, *args: Any) -> None:
        pass  # Never log request paths, payloads, or credentials in this demo.

    def reply(self, status: int, payload: dict[str, object]) -> None:
        body = json.dumps(payload, separators=(",", ":")).encode()
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.send_header("Connection", "close")
        self.end_headers()
        self.wfile.write(body)
        self.close_connection = True

    def do_GET(self) -> None:  # noqa: N802 - standard-library handler contract
        if self.path == "/health":
            self.reply(200, {"ok": True})
        else:
            self.reply(404, {"error": "not_found"})

    def do_POST(self) -> None:  # noqa: N802 - standard-library handler contract
        if self.path != "/webhook":
            self.reply(404, {"error": "not_found"})
            return
        if self.headers.get("Transfer-Encoding"):
            self.reply(400, {"error": "content_length_required"})
            return
        lengths = self.headers.get_all("Content-Length", [])
        try:
            if len(lengths) != 1:
                raise ValueError
            length = int(lengths[0])
        except ValueError:
            self.reply(411, {"error": "content_length_required"})
            return
        if not 0 < length <= MAX_BODY:
            self.reply(413, {"error": "body_size_out_of_range"})
            return
        if self.headers.get_content_type() != "application/json":
            self.reply(415, {"error": "expected_application_json"})
            return
        try:
            data = self.rfile.read(length)
            if len(data) != length:
                raise ValueError
            payload = json.loads(data)
            if not isinstance(payload, dict):
                raise ValueError
        except (ValueError, UnicodeError, TimeoutError):
            self.reply(400, {"error": "invalid_json"})
            return
        event = payload.get("event")
        self.reply(200, {"received": True, "event": event if isinstance(event, str) else None})


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--port", type=int, default=8000)
    args = parser.parse_args()
    if not 1 <= args.port <= 65535:
        parser.error("port must be between 1 and 65535")
    server = ThreadingHTTPServer(("127.0.0.1", args.port), WebhookHandler)
    print(f"Listening on http://127.0.0.1:{args.port}/webhook", flush=True)
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    finally:
        server.server_close()


if __name__ == "__main__":
    main()

2. Test the receiver locally

In a second terminal, send a sample event. In Windows PowerShell, use curl.exe instead of curl for these commands.

Create event.json in the same directory with this content:

{"event":"task.completed","source":"local-demo"}

Then send the request:

curl -i -X POST http://127.0.0.1:8000/webhook -H "Content-Type: application/json" --data-binary @event.json

Expected result: HTTP 200 and:

{"received":true,"event":"task.completed"}

If the local request fails, fix that before starting the tunnel. Hokio forwards to your application; it does not start that application for you.

3. Install and connect Hokio

Windows

Get Hokio from the Microsoft Store ↗. Use the device connection instructions shown by your Hokio workspace. Select local application port 8000 to match the receiver.

Linux

The installer detects your architecture and Linux environment and verifies the selected release checksum:

curl -fsSL https://hokio.dev/download/install.sh | sh

With an active workspace, ask your workspace owner for a one-time device connection code, or generate one from Connect a device if your role allows it. Then run:

hokio-agent login --panel-url https://hokio.dev --port 8000 --connect

Enter the code when prompted; avoid putting it in a shared command or screenshot. The connector displays your assigned webhook_url and keeps the foreground connection running. Codes expire and can be redeemed only once.

Your assigned URL comes from Hokio. Do not copy an illustrative hostname from a screenshot. Keep the receiver and connector running during the test.

Reconnect later

hokio-agent connect
hokio-agent status

Your assigned webhook address remains the same while the endpoint stays assigned. If you change the application port, update the connector to use the actual listening port:

hokio-agent config --port 8000

4. Send an event to your public HTTPS URL

Replace YOUR_ASSIGNED_HOKIO_HOST below with the hostname returned by your connector. The placeholder is not a working endpoint. Keep the /webhook path:

curl -i -X POST https://YOUR_ASSIGNED_HOKIO_HOST/webhook -H "Content-Type: application/json" --data-binary @event.json

A successful connection returns the same HTTP 200 receipt as the localhost test. You can also send this request from a second machine to verify that the URL is publicly reachable. Use synthetic event data for the test.

Paste the full URL, including /webhook, into the service that sends your AI callbacks or automation events. Configure the endpoint's allowed methods, accepted paths, source IP rules, and limits to fit that service.

Troubleshooting

Local connection refused
Check that the Python receiver is running on the same machine as the connector and on port 8000. If the port is occupied, choose a free port in the receiver and use that same port in Hokio.
The public URL returns unavailable
Check hokio-agent status, the internet connection, and whether the connector and local application are running. The URL stays assigned while offline, but requests are not queued for later delivery.
The request is denied
Ask your workspace owner to check source IP restrictions, HTTP methods, accepted paths, and traffic limits. Keep the policy restricted to the intended sender.
The code is rejected
Request a fresh one-time connection code from your workspace owner. An expired or already redeemed code cannot register another device.
Can I connect without an account?
No. Reading these guides and running the local receiver does not create a Hokio account or reserve a URL.

Keep building.

Read the Hokio Quick Start →

Get notified when registration opens →