# audiopen-webhook Zero-dependency Python HTTP server that receives AudioPen webhook POSTs and writes bare markdown files to `inbox/raw/`. The separate `scripts/audiopen-ingest.sh` wrapper normalises those files into proper fleeting notes under `inbox/`. ## Why this shape - **Receiver is minimal (stdlib-only http.server).** No Flask, no runtime to keep updated, ~150 LOC. Reads AudioPen's JSON payload, writes one file, logs to stderr. Anything fancier belongs in the ingest wrapper. - **Two-step raw → normalised.** Keeps the receiver stupid; all schema decisions (frontmatter, slug format, filename) live in one shell script you can read in 30 seconds. - **Designed for Tailscale Funnel.** Binds to 127.0.0.1 by default. Tailscale Funnel (or Cloudflare Tunnel, or an SSH remote forward, or ngrok) terminates the public endpoint and proxies to 127.0.0.1:8765. No ports opened on your router; nothing behind Authentik. ## Setup Full step-by-step lives in [`docs/setup/audiopen.md`](../../docs/setup/audiopen.md). Quick version: 1. **Generate a secret**: `head -c 24 /dev/urandom | base64 | tr -d '/+='`. 2. **Configure AudioPen** to POST JSON to `https://.ts.net/audiopen/`. 3. **Install the systemd user units**: ```sh mkdir -p ~/.config/audiopen-webhook ~/.config/systemd/user echo "AUDIOPEN_WEBHOOK_SECRET=" > ~/.config/audiopen-webhook/env chmod 600 ~/.config/audiopen-webhook/env cp audiopen-webhook.service.example ~/.config/systemd/user/audiopen-webhook.service # create two more systemd files for the ingest path/service per # audiopen-ingest.path.example systemctl --user daemon-reload systemctl --user enable --now audiopen-webhook.service audiopen-ingest.path ``` 4. **Expose via Tailscale Funnel**: ```sh tailscale funnel --bg 8765 ``` 5. **Test**: ```sh curl -X POST -H 'Content-Type: application/json' \ -d '{"title":"Test","body":"Hello from curl"}' \ https://.ts.net/audiopen/ # → OK ls ~/path/to/vault/inbox/raw # should see the test file # wait a beat for the inotify path unit to fire ls ~/path/to/vault/inbox # should see the normalised file ``` ## Payload shape The receiver accepts any of these JSON keys for the title: `title`, `name`. For the body: `body`, `output`, `summary`, `polished`, `orig_transcript`. Unknown keys are ignored; the raw payload is logged to stderr so you can see what AudioPen actually sent if shape drifts. If AudioPen's webhook format changes in a way the current extractor misses, check `journalctl --user -u audiopen-webhook.service` for the logged shape and update the key list in `extract_content()` at the top of `main.py`. ## Alternative: email + IMAP fetch If running a public-reachable receiver isn't viable, the plan has an email-path fallback documented in `docs/setup/audiopen.md`. It's entirely outbound: AudioPen emails each note to a dedicated address, and a cron on the desktop pulls via IMAP and drops files in `inbox/raw/`. Same ingest wrapper handles the rest.