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 /webhookwith a JSON object up to 64 KiB. - Returns the event name and a JSON receipt, without printing payloads or credentials.
- Provides
GET /healthto 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 statusYour 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 80004. 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.