otterkit
Instant tunnels and webhooks for AI agents. Pay per use with prepaid credits.
Quick Start
npx otterkit login
npx otterkit tunnel 3000
Buy credits at console.otterkit.com. Once you've logged in,
agents on the same machine provision automatically using your credits.
Commands
Tunnel
Expose a local port to the internet.
npx otterkit tunnel <port>
npx otterkit tunnel <port> --log
npx otterkit tunnel <port> --daemon
npx otterkit tunnel <port> --daemon --ttl 3d
npx otterkit tunnel <port> --subdomain myapp
npx otterkit tunnel <port> --auth user:pass
--auth is enforced by the CLI on your machine, before anything reaches your local
server - unauthenticated requests get a 401 and are never forwarded. Credentials
are not sent to or stored by OtterKit's servers.
Stable URLs
By default each tunnel gets a fresh random URL (https://tunnel-a1b2c3d4.otterkit.app).
Pass --subdomain <name> to get a stable, reserved URL instead. The name is claimed to
your account the first time you use it and reused on every run after that - so the same
public URL (https://myapp.otterkit.app) always points at your tunnel, across machines and
restarts. It works on tunnel and webhook, foreground and --daemon.
Holding a name is free - you still only pay the normal 1 credit/hr while a tunnel is
connected. If another account already owns the name you'll get a "taken" error and no credit
is charged; pick another name or omit the flag for an auto name.
npx otterkit subdomains
npx otterkit subdomains reserve myapp
npx otterkit subdomains release myapp
Project Config (otterkit up)
Define named tunnel profiles in an otterkit.toml and bring them all up with one command.
Every profile runs as a background daemon, so status, inspect, and stop work as usual.
[tunnels.web]
port = 3000
subdomain = "myapp"
auth = "admin:s3cret"
log = true
ttl = "8h"
[tunnels.hooks]
webhook = true
subdomain = "myapp-hooks"
respond = 200
respond_body = '{"ok":true}'
npx otterkit up
npx otterkit up --json
npx otterkit down
npx otterkit up --config ./deploy/otterkit.toml
Webhook
Capture incoming HTTP requests without a local server. Every request is saved to
~/.otterkit/requests/<subdomain>.jsonl so nothing is lost when the terminal closes.
npx otterkit webhook
npx otterkit webhook --daemon --ttl 4h
Some providers require a specific response before they deliver events (challenge echoes,
strict 2xx checks). Customize the auto-response with --respond / --respond-body
(default: 200 {"received":true}):
npx otterkit webhook --respond 204
npx otterkit webhook --respond 200 --respond-body '{"challenge":"accepted"}'
With --standby, the endpoint stays live while you're disconnected (laptop closed,
daemon killed): the server answers each request with your auto-response and buffers up
to 200 captures (64 KB bodies), replaying them into your local log on reconnect -
providers never see downtime. Billing continues while disconnected (still capped at
10 credits/day; the session still auto-stops at its --ttl).
npx otterkit webhook --standby
npx otterkit webhook --daemon --standby --respond 204
Inspect
View requests captured by webhook or tunnel --log.
npx otterkit inspect <subdomain>
npx otterkit inspect <subdomain> --json
npx otterkit inspect <subdomain> --last 50
npx otterkit inspect <subdomain> --follow
npx otterkit inspect <subdomain> --method POST --status 5xx --path /webhook
npx otterkit inspect <subdomain> --har > session.har
Filters (--method, --status, --path) combine and work with --json, --follow,
and --har. --status accepts an exact code (500) or a class (5xx).
Replay
Re-send a captured request to your local server - same as the desktop app's replay,
straight to the target with no tunnel round-trip. Great for re-testing webhook handlers
after a fix without waiting for the provider to fire again. The replayed exchange is
appended to the capture log, so it shows up in otterkit inspect.
The target defaults to the running daemon's host:port for that subdomain; pass
--target for webhook captures or stopped tunnels.
npx otterkit replay <subdomain>
npx otterkit replay <subdomain> --index 3
npx otterkit replay <subdomain> --target 127.0.0.1:3000
npx otterkit replay <subdomain> --json
npx otterkit replay <subdomain> --method PUT --path /v2/hook \
-H "X-Debug: 1" --body '{"event":"payment.failed"}'
npx otterkit replay <subdomain> --set data.amount=999 --set data.livemode=false
Capture logs record the full exchange - response status, headers, and body - so
inspect --json and --har exports carry both sides of every request.
Await
Block until a matching request arrives - the scriptable primitive for CI and agents:
"trigger the event, await its arrival, assert on the payload." By default it keeps
streaming matches (your session keeps running either way - await never stops the
tunnel it watches). Pass --count for a deterministic exit.
npx otterkit await <subdomain>
npx otterkit await <subdomain> --count 1 --timeout 120s
npx otterkit await <subdomain> --method POST --path /stripe --json
Exit codes: 0 when --count is satisfied (or on Ctrl+C), 2 when --timeout
expires unmet. Filters (--method, --status, --path) match inspect's.
Continue a /try session
Started on otterkit.com/try? The page shows a claim
command that moves the session into your terminal - same public URL, and everything
already captured on the page is imported into your local log:
npx otterkit webhook --claim <code>
Send & Verify Events
Develop a webhook handler with no provider account, dashboard, or real event. send fires a
correctly-signed synthetic event at your local handler; verify checks whether captured requests
carry a valid signature. Both run locally - the secret never leaves your machine. Schemes: Stripe,
GitHub, Shopify, Slack.
npx otterkit send stripe:payment_intent.succeeded 127.0.0.1:3000/webhooks/stripe --secret whsec_...
npx otterkit send github:push 127.0.0.1:3000/hooks --secret ghs_...
npx otterkit send --list
npx otterkit verify myhooks stripe --secret whsec_...
npx otterkit replay myhooks --set data.object.amount=999 --resign stripe --secret whsec_...
MCP Server (for AI agents)
otterkit mcp runs a Model Context Protocol server over stdio - Claude Code,
Cursor, and any MCP client get tunnels, webhooks, capture logs, await, and
replay as native tools, authenticated by the same otterkit login.
claude mcp add otterkit -- npx otterkit mcp
{ "mcpServers": { "otterkit": { "command": "npx", "args": ["otterkit", "mcp"] } } }
Tools: webhook_create, tunnel_create, sessions_list, session_stop,
requests_list, request_await, request_replay, event_send, request_verify,
try_claim, account_status.
Account
npx otterkit login
npx otterkit whoami
npx otterkit balance
npx otterkit logout
Manage Daemons
npx otterkit status
npx otterkit stop <subdomain>
JSON Output (for agents & scripts)
Every read/provision command takes --json and prints one machine-readable result:
npx otterkit tunnel 3000 --daemon --json
npx otterkit webhook --daemon --json
npx otterkit up --json
npx otterkit status --json
npx otterkit subdomains list --json
npx otterkit inspect <subdomain> --json
npx otterkit replay <subdomain> --json
npx otterkit whoami --json
npx otterkit balance --json
Errors in --json mode are also JSON ({"error":"insufficient_credits", ...}) with exit
code 1.
Options
--host <host> | Local host to forward to | 127.0.0.1 |
--log | Capture requests to a JSONL log (tunnel only) | off |
--daemon | Run in background | off |
--ttl <duration> | Auto-stop after duration, e.g. 4h, 3d (max 7d) | 24h |
--subdomain <name> | Use a stable reserved URL (claimed on first use) | auto |
--auth <user:pass> | Require HTTP Basic auth (tunnel only, enforced locally) | off |
--respond <status> | Webhook auto-response status (webhook only) | 200 |
--respond-body <d> | Webhook auto-response body (webhook only) | {"received":true} |
--claim <code> | Continue a /try page session (webhook only) | - |
--set <path=value> | Edit a JSON body field before replaying (repeatable) | - |
--count <n> | await: exit 0 after N matches | stream forever |
--timeout <dur> | await: give up after e.g. 30s, 5m (exit 2) | none |
--json | Machine-readable output (daemon/read commands) | off |
Pricing
Metered by connected time. 1 credit = $0.01. Buy prepaid credits in the console.
| Per connected hour | 1 credit ($0.01), first hour up front |
| Daily cap per tunnel | 10 credits ($0.10) - hours beyond are free |
| While disconnected | Free - billing pauses |
| Auto-stop | Default 24h, --ttl up to 7d |
A forgotten tunnel bills at most the daily cap, then auto-stops at its TTL.
How It Works
otterkit login runs a browser device-flow and saves an API token to ~/.otterkit/credentials.json.
- The CLI sends that token when provisioning; the server debits your credit balance.
- Server provisions a public URL like
https://tunnel-a1b2c3d4.otterkit.app (or your
reserved --subdomain name, if given).
- CLI connects via WebSocket and forwards traffic to your local port.
For headless/CI, set OTTERKIT_TOKEN (create a token at console.otterkit.com) instead of otterkit login.
Links
License
MIT