The most common confusion in mobile API testing, untangled: why localhost fails on a
phone, the three ways to fix it, and how webhooks fit in.
localhost always means "this device" — on your phone it points at
the phone. Use your laptop's LAN IP on the same Wi-Fi, Tailscale from anywhere, or a tunnel
when the internet must reach you.
Find your laptop's address (macOS: ipconfig getifaddr en0 · Windows:
ipconfig · Linux: hostname -I) and call
http://192.168.1.42:3000 from the phone instead of localhost:3000.
Two gotchas cause 90% of the "still refused" cases: your dev server must listen on all
interfaces, not just loopback (vite --host, next dev -H 0.0.0.0,
flask run --host=0.0.0.0, php -S 0.0.0.0:8000…), and the OS firewall
must allow incoming connections for it. On iOS, accept the Local Network permission
when prompted — without it every LAN request fails silently.
Install Tailscale on laptop and phone, sign in with the same account, done: each device gets a
stable private address (100.x.y.z, or a MagicDNS name like
my-macbook). Your phone now reaches the dev server from the couch, the office or
cellular — no port forwarding, nothing exposed to the public internet, free for personal use.
This is the "it just works every day" option for testing against your own machines.
ngrok http 3000 or cloudflared tunnel --url http://localhost:3000
gives you a public HTTPS URL that forwards to your laptop. This is the tool for
receiving webhooks: Stripe, GitHub or Twilio can't call
192.168.1.42 — they need a real URL. Remember the trade-off: while the tunnel is
up, that URL is reachable by anyone who has it, so treat it as temporary and rotate it.
Receiving: tunnel → paste the public URL into the provider's webhook settings →
watch requests arrive in your server logs. Sending & replaying: no tunnel
needed at all. Take the provider's payload (their docs ship samples), build the POST with the
right Content-Type and signature headers, and fire it at your endpoint — from
ReqPad this works against a LAN IP, a Tailscale address or a tunnel URL alike,
with the request saved to history so replaying a failing webhook is one tap, on the train,
without the laptop. The cURL converter and
Base64 tool help when the provider docs hand you raw examples.
Because localhost always means "this device". On your phone, localhost:3000 points at the phone itself, where nothing is listening. You need your laptop's LAN IP (192.168.x.x), a private VPN address (Tailscale), or a public tunnel URL (ngrok / cloudflared).
Two usual suspects: the dev server is bound to 127.0.0.1 only (start it with --host 0.0.0.0 or your framework's equivalent), or the laptop firewall is blocking incoming connections. On iOS, also accept the Local Network permission prompt when your API client first touches a LAN address.
Only for receiving them from third-party services — Stripe or GitHub cannot reach your laptop, so you expose it with ngrok or cloudflared and give them that URL. For sending or replaying webhook payloads against your own endpoint, no tunnel is needed: build the POST with the provider's JSON body and signature headers and send it directly.
Different jobs. Tailscale gives your devices a private network: the phone reaches the laptop from anywhere, nothing is public — ideal for day-to-day dev. Tunnels (ngrok, cloudflared) make a port temporarily public — required when an external service must call you, with the bill that anyone with the URL can too.
ReqPad tests against LAN IPs, Tailscale addresses and tunnel URLs — six protocols, real auth, request history. Free to start.