Prompt: receive third-party webhooks on your dev machine at a stable URL
Have an agent give a local dev server a permanent public HTTPS address
for Stripe, GitHub, Twilio or any other webhook sender — through an ird
tunnel that survives reboots via the background agent — with signature
verification wired in locally, a way to tell "the tunnel was down" from
"my handler failed", and a teardown that actually stops the bill.
Needs: the ird CLI logged in on the machine that runs the dev server (the MCP server can create the tunnel, but forwarding runs where the app is); a hostname on a zone hosted on ird DNS; access to the provider's dashboard
Costs: hourly while the tunnel exists (credits_prices → recurring.tunnel-hourly), plus per MB stored if a custom offline page is set (recurring.ipfs-pin-hourly)
Group: developer workflow
Paste everything below the line into a fresh conversation. Fill in the
FILL IN block first.
Task
Expose the local route below at https://HOST/PATH, keep it reachable
across restarts, register it at PROVIDER with signature verification,
and delete the tunnel when I say so. Nothing is created before I
confirm the hourly cost.
FILL IN
HOST: hooks.example.ro # hostname on a zone hosted on ird DNS
PORT: 4000 # local port the dev server listens on
PATH: /webhooks/stripe # route the provider will POST to
PROVIDER: stripe # stripe | github | twilio | other (name it)
EVENTS: checkout.session.completed # what to subscribe to at the provider
SECRET_VAR: STRIPE_WEBHOOK_SECRET # env var holding the signing secret
OFFLINE: standard # or a folder with index.html, served while offline
Non-negotiable rules
- Hourly until deleted. Bills from creation until
delete;
ird tunnels stop and ird agent stop only stop forwarding, the
tunnel keeps billing. Show hourly and hourly × 730 from
credits_prices before creating, wait for a yes.
- Never trust an unsigned payload. Verify the provider's
signature over the raw body before doing anything. The signing secret
lives in SECRET_VAR (a local
.env, not committed), never in chat,
logs or the repo.
- One tunnel per hostname.
list_tunnels first; reuse an existing
one on HOST rather than creating a second.
- Only expose a listening port.
curl -sI http://127.0.0.1:PORT
must answer first.
- The provider dashboard is mine. Give me the exact values to
enter; never use provider API keys I didn't hand you.
Steps
A. Preflight. dns_list_zones must include HOST's zone (else stop:
register-domain / bring-domain-dns prompts). list_tunnels for an
existing one. Rules 4 and 1.
B. Create and keep connected. On the dev machine:
ird tunnel HOST PORT
ird agent start
ird agent autostart on
ird tunnel creates the tunnel, writes the DNS record and starts
forwarding; the agent keeps it connected, autostart on restores it at
login. Check ird agent status and ird tunnels status HOST.
MCP-only: create_tunnel(hostname=HOST, ssh_public_key=<device key>)
returns the ssh_command the device runs — relay verbatim.
C. Register at PROVIDER. URL https://HOST/PATH, events EVENTS;
secret into SECRET_VAR:
- stripe: Developers → Webhooks → add endpoint; copy the
whsec_…
secret; header Stripe-Signature.
- github: Settings → Webhooks; content type
application/json; a
secret you generate (openssl rand -hex 32); header
X-Hub-Signature-256.
- twilio: the URL goes on the number/service;
X-Twilio-Signature uses
the auth token and covers the full URL — exactly https://HOST/PATH.
D. Verify locally. In the handler: read the raw body (before any
JSON middleware), verify with the provider's SDK or HMAC helper using
SECRET_VAR, 400 on mismatch, return 2xx fast, do the work
asynchronously. Log headers and raw body of every request to a
file: ird tunnels logs shows forwarder events only (connected,
disconnected, "nothing is listening on localhost:PORT"), never
requests.
E. Test end to end. Trigger a test delivery (Stripe "send test
event", GitHub "Redeliver" on the ping, Twilio a real call/SMS). Expect:
list_tunnels shows HOST active, your log shows a validly signed
request, the provider's delivery log shows 2xx. Then tamper one byte of
a replay and confirm a 400.
F. Offline behaviour. If OFFLINE is a folder:
tunnel_set_offline(tunnel=HOST, path=<absolute folder>) (CLI:
ird tunnel offline HOST ./folder) — served whenever the forwarder is
not connected, effective within a minute. Laptop closed = the provider
reaches that page, not your handler. Its HTTP status is undocumented,
so test once: ird tunnels stop HOST, redeliver, read the provider's
log, restart. A recorded 2xx means missed events must be reconciled
from the provider's API; a failure means redeliver from its dashboard
once ird agent status shows forwarding. ird tunnels logs timestamps
every drop and reconnect.
G. Teardown (when I say "done"). Remove the endpoint at PROVIDER
first (or it keeps retrying), then delete_tunnel(tunnel_id) from
list_tunnels (CLI: ird tunnels delete HOST) — billing stops
immediately, the hostname stops serving. DNS and offline-page cleanup
are undocumented: dns_get_zone, dns_delete_record a leftover HOST
record; if OFFLINE was a folder, ipfs_pins → ipfs_unpin any
surviving pin. ird agent autostart off if this was its only tunnel.
Deliverable
https://HOST/PATH, the tunnel id and status, the agent status line,
hourly/monthly cost, the provider-side settings (URL, events,
header name — never the secret), the test-delivery result, and the
one-line teardown command.