Text your infrastructure. It texts back.
Message your pingwa number — or send a voice note — and your own code answers. pingwa POSTs a signed webhook to your handler, your handler does the thing, and it replies over the same chat. No app, no linked device, no dashboard to open; a WhatsApp thread with your servers. Replies inside the 24h window are free, so a whole back-and-forth with your infra costs nothing.
The whole loop
One direction is the notify path you already know. This page
is the inbound half — you talk first, your code reacts. Register an
HTTPS endpoint once with POST /v1/webhooks; from then on every
message you send is pushed to it.
your handler ──do the thing──▶ POST /v1/notify ──▶ reply lands in your chat (free — window is open, you just messaged)
Registering prints a signing secret — whsec_… —
once. Store it; it signs every delivery and is the only way
to know a request is really from pingwa and not a stranger who found your URL.
The handler (FastAPI, ~20 lines)
Verify the signature over the raw body bytes with
hmac.compare_digest, then dispatch on the first word. Here
note … appends a line to a file and status reports
uptime back into the chat. The reply is a plain POST /v1/notify —
free, because your own message just opened the 24h window.
import hmac, hashlib, os, subprocess, httpx from fastapi import FastAPI, Request, HTTPException SECRET = os.environ["PINGWA_WHSEC"].encode() # the whsec_... from registration KEY = os.environ["PINGWA_KEY"] # pw_... to send replies app = FastAPI() def reply(text): httpx.post("https://pingwa.dev/v1/notify", headers={"Authorization": f"Bearer {KEY}"}, json={"text": text}) @app.post("/pingwa-hook") async def hook(request: Request): raw = await request.body() # EXACT bytes — do not re-parse then re-dump want = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest() got = request.headers.get("X-Pingwa-Signature", "") if not hmac.compare_digest(want, got): raise HTTPException(401) # reject anything that fails body = (await request.json()).get("body") or "" cmd, _, rest = body.strip().partition(" ") if cmd == "note": open("inbox.txt", "a").write(rest + "\n") reply("noted ✍️") elif cmd == "status": reply("up " + subprocess.check_output(["uptime", "-p"]).decode().strip()) return {"ok": True} # any 2xx acknowledges the delivery
The inbound payload is
{"event":"inbound_message","message_id","body","button_id",
"reply_to_message_id","wa_message_id","window_open","created_at"}.
body is your text; window_open tells you the reply
is free right now (it always is, straight after you messaged). Return any
2xx or pingwa retries with backoff — after three exhausted deliveries it
deactivates the webhook, so keep the handler honest.
Voice notes
Send a voice message instead of typing and the payload gains
a media object; body is empty for a pure voice note.
pingwa forwards the audio — transcription is on your side, so you own
the model and the words never leave your box.
# the media object on a voice message: # {"kind":"audio","voice":true,"mime_type":"audio/ogg; codecs=opus", # "url":"https://.../v1/media/<message_id>?exp=&sig="} media = payload.get("media") if media and media["kind"] == "audio": # the signed url authorizes the fetch — no api key needed — but it is # short-lived (~1h), so pull the bytes right away, don't queue it for later audio = httpx.get(media["url"]).content open("note.ogg", "wb").write(audio) # transcribe on your side (openai-whisper's Python API returns the text; # the CLI writes it to a file instead — whisper.cpp works the same way): import whisper text = whisper.load_model("base").transcribe("note.ogg")["text"] # then treat the transcript exactly like a typed command (dispatch as § 2)
Honest about the boundary: pingwa hands you the raw
audio/ogg opus bytes and stops there. Whether you run
local whisper.cpp, the openai-whisper package, or a cloud STT is your call —
the recipe just shows the seam where the transcript becomes a command.
GTD capture — message yourself
The command-bus doubles as a capture inbox. Anything you text that isn't a command falls through to a catch-all that appends it to a todo file — a thought from a walk, a link, a "buy milk", captured from anywhere your phone has signal, in two taps. No app to open, no context to switch into; the chat you already have with your infra is the inbox.
KNOWN = {"note", "status"}
if cmd not in KNOWN:
# not a command → it's a capture; the whole line goes to the inbox
open("inbox.txt", "a").write(body.strip() + "\n")
reply("📥 captured")
Voice-note capture is the same seam plus § 3: talk into your
phone, whisper turns it into a line, the line lands in inbox.txt.
Frictionless capture is the whole point of GTD — this is the shortest path to
it that still runs on your own hardware.
Every message you send resets the free 24h window, and replies inside that window are free and unlimited. So an entire conversation with your infrastructure — ask, answer, follow-up, answer — costs nothing. The paid meter only ticks when your code taps you unprompted outside the window; a bus you drive by texting first stays free.
FAQ
WhatsApp webhook automation — how does it work here?
Register an HTTPS endpoint with POST /v1/webhooks and pingwa
POSTs every inbound message to it as
{"event":"inbound_message","body",…}. Your handler verifies the
X-Pingwa-Signature header (HMAC-SHA256 over the raw body),
dispatches on the text, and replies with POST /v1/notify. That
is the whole automation — § 2 is the complete handler, no framework of ours
to learn.
Can I text my server / run commands from WhatsApp?
Yes — that's the recipe. Your first word is the command
(note, status, or whatever you define) and your
code decides what it does: run a script, restart a service, query a
database, kick off a deploy. Keep the command set small and explicit, and
remember the signature check in § 2 is what stops anyone but pingwa from
reaching that switch.
Voice note to automation — does pingwa transcribe it?
No — pingwa forwards the audio, transcription is on your side. A voice
message adds a media object with a signed, ~1h URL; you GET the
audio/ogg opus bytes and pipe them to whisper (local
whisper.cpp or the openai-whisper package) yourself, then treat the
transcript as a typed command. That keeps the words on your box and lets you
pick the model — see § 3.
How do I verify a delivery is really from pingwa?
Recompute "sha256=" + HMAC-SHA256(secret, raw_body).hexdigest()
and compare it to the X-Pingwa-Signature header with
hmac.compare_digest. Reject anything that fails. The secret is
the whsec_… shown once when you registered the webhook — it
never appears in a payload, so a request that carries a valid signature
could only have come from someone who holds it. Compute the HMAC over the
exact raw bytes; re-parsing and re-serializing the JSON will change them and
break the match.
Scan the code and get instant access. It opens WhatsApp with “join” ready to send, and your API key comes straight back.
The handler above is the honest shape of the integration, not lab-verified for every framework and STT stack — pin your versions, keep the command set small, and test the signature check before you wire anything destructive behind a text message.