API
Everything you can click is also an API call. Create a token on your account page, then send it as Authorization: Bearer whx_…. JSON in, JSON out. The machine-readable document is at /api/openapi.json.
The same API is also served on its own host, https://api.webhooks.xyz. Use it from scripts and browser apps: it has no pages or cookies, only /api/v1. Both hosts give the same answers.
Limits
600 reads and 60 writes a minute per token. Over the limit you get 429. Scopes: read for GET, write for settings, deliver for replay.
Bins
GET https://webhooks.xyz/api/v1/bins
POST https://webhooks.xyz/api/v1/bins
GET https://webhooks.xyz/api/v1/bins/{bin}
DELETE https://webhooks.xyz/api/v1/bins/{bin}
PUT https://webhooks.xyz/api/v1/bins/{bin}/response {"status": 503, "headers": {"Content-Type": "text/plain"}, "body": "down", "delay_ms": 0}
PUT https://webhooks.xyz/api/v1/bins/{bin}/scenario {"scenario": "fail", "fail_minutes": 10} # normal | timeout | fail | echo
Requests
GET https://webhooks.xyz/api/v1/bins/{bin}/requests?method=POST&verify_status=failed&since=1759924800&path_prefix=/orders&q=ORD-20481&after=0&limit=100
GET https://webhooks.xyz/api/v1/bins/{bin}/requests/{id}
DELETE https://webhooks.xyz/api/v1/bins/{bin}/requests
Each request has headers, body (or body_b64 for binary), verify (status ok | failed | unchecked, scheme, detail), resp_status and rule_hit.
Signature verification
GET https://webhooks.xyz/api/v1/bins/{bin}/secrets # also lists the schemes
POST https://webhooks.xyz/api/v1/bins/{bin}/secrets {"scheme": "stripe", "secret": "whsec_…", "label": "live"}
DELETE https://webhooks.xyz/api/v1/bins/{bin}/secrets/{id}
Destinations and deliveries
GET https://webhooks.xyz/api/v1/bins/{bin}/destinations
POST https://webhooks.xyz/api/v1/bins/{bin}/destinations {"url": "https://api.acme.example/hooks", "method": "POST", "headers": {"Authorization": "Bearer …"}, "timeout_s": 10}
DELETE https://webhooks.xyz/api/v1/bins/{bin}/destinations/{id}
POST https://webhooks.xyz/api/v1/bins/{bin}/destinations/{id}/pause?paused=1
GET https://webhooks.xyz/api/v1/bins/{bin}/deliveries?status=failed&request_id=…
POST https://webhooks.xyz/api/v1/bins/{bin}/deliveries/{id}/replay
POST https://webhooks.xyz/api/v1/bins/{bin}/deliveries/replay-failed
Import and export
GET /api/v1/bins/{id}/export → {format, bin: {name, response, destinations}, requests: [...]} (settings + every request)
POST /api/v1/import {"source": "webhook-site", "token": "…", "api_key": "…", "bin_id": null}
{"source": "hookdeck", "api_key": "…"} | {"source": "pipedream", "api_key": "…", "source_id": "dc_…"}
{"source": "svix-play", "token": "…"} | {"source": "json" | "har", "data": {…}, "bin_id": "…"}
→ {id, bins: [{id, requests, url}], requests_n, settings_n, skipped, notes}
GET /api/v1/imports → {imports: [...]} (your import history with reports)
Status feeds
GET /api/v1/status-feeds → {subscriptions: [...], vendors: [{slug, name, url}]}
POST /api/v1/status-feeds {"vendor": "github", "connection_ids": [12], "keywords": "Actions", "updates": true}
{"kind": "webhook", "name": "Acme Cloud", "connection_ids": [12], "min_impact": "major"} → inbound_url
DELETE /api/v1/status-feeds/{id}
Private Relay
A destination with "mode": "relay" is called by whx relay from inside your network; private addresses are accepted. Relay tokens (whr_…) are per bin and deliver-only. Private Relay has the agent protocol.
POST /api/v1/bins/{id}/destinations {"url": "http://10.0.3.12:8080/webhooks", "mode": "relay"}
GET /api/v1/bins/{id}/relay/tokens → {"tokens": [{id, name, prefix, connected, last_seen, agent}]}
POST /api/v1/bins/{id}/relay/tokens {"name": "vpc-eu-1"} → {"token": "whr_…", "run": "whx relay --token whr_…"} (token shown once)
DELETE /api/v1/bins/{id}/relay/tokens/{tid}
Public: a bin without a token
POST /api/v1/public/bins needs no token and no body. It makes an anonymous bin and answers 201 {"id", "url", "page", "expires_at", "retention"}. The bin is deleted 24 hours after its last request. Browsers may call it only from the sites we list (today mailt.in). After some bins from one network in an hour the answer is 429 {"error": "captcha_required", "new_url": "https://webhooks.xyz/new"}: open new_url, where a short check makes the bin.
Connections and actions
GET https://webhooks.xyz/api/v1/connections
POST https://webhooks.xyz/api/v1/connections {"kind": "slack", "label": "#revenue", "config": {"webhook_url": "https://hooks.slack.com/services/…"}}
DELETE https://webhooks.xyz/api/v1/connections/{id}
GET https://webhooks.xyz/api/v1/bins/{bin}/actions
POST https://webhooks.xyz/api/v1/bins/{bin}/actions {"trigger": "request", "connection_id": 1, "filter": "body.type is payment_intent.succeeded", "template": "Paid {{ body.data.object.amount_received }} by {{ body.data.object.receipt_email }}"}
POST https://webhooks.xyz/api/v1/bins/{bin}/actions/{id}/test # fires once against the newest request
DELETE https://webhooks.xyz/api/v1/bins/{bin}/actions/{id}
GET https://webhooks.xyz/api/v1/me
Filters and templates
A filter is one condition per line: field op value. Ops: is, contains, starts, exists. Fields: method, path, verify, header:x-github-event, query:source, body.data.object.currency. All lines must match.
A template is text with {{ field }} placeholders from the same fields, plus {{ bin_url }}, {{ request_id }}, {{ event.error }}, {{ event.failures_in_a_row }} and {{ destination }} on delivery triggers. No code runs in templates.
Example: nightly list of failed deliveries
curl -s -H "Authorization: Bearer $WHX_TOKEN" \
"https://webhooks.xyz/api/v1/bins/k7f3abc/deliveries?status=failed" | jq '.deliveries[] | {id, request_id, error}'