Recipes: test a webhook in CI or from an agent
Copy-paste snippets. Each one creates a bin, triggers a webhook, and asserts on exactly what arrived — no browser, no signup. This is what Catchr is built for.
The 3-step pattern
- Create a bin → get a
url+token. - Point your webhook / callback / agent tool at
url. - Read
/api/bins/:id/requests?token=…and assert on the payload.
🐚 Bash / any CI (curl + jq)
set -e
BIN=$(curl -s -X POST https://catchr.perch-app.workers.dev/api/bins)
ID=$(echo "$BIN" | jq -r .id); TOKEN=$(echo "$BIN" | jq -r .token)
URL=$(echo "$BIN" | jq -r .url)
# ... trigger whatever fires a webhook at "$URL" ...
curl -s "$URL" -H 'content-type: application/json' -d '{"event":"deploy.finished"}'
# assert it arrived with the right body
BODY=$(curl -s "https://catchr.perch-app.workers.dev/api/bins/$ID/requests?token=$TOKEN" | jq -r '.requests[0].body')
echo "$BODY" | jq -e '.event == "deploy.finished"' > /dev/null \
&& echo "✅ webhook received" || { echo "❌ missing webhook"; exit 1; }
🐍 Python (pytest)
import requests, time
BASE = "https://catchr.perch-app.workers.dev"
def test_my_webhook_fires():
bin = requests.post(f"{BASE}/api/bins").json()
url, id_, token = bin["url"], bin["id"], bin["token"]
# trigger the thing under test that should POST to `url`
my_app.send_notification(webhook_url=url, event="user.created")
# long-poll: one blocking call returns the instant the webhook lands
reqs = requests.get(f"{BASE}/api/bins/{id_}/requests",
params={"token": token, "wait": 15}).json()["requests"]
assert reqs, "no webhook received"
import json
assert json.loads(reqs[0]["body"])["event"] == "user.created"
The wait=15 long-poll replaces the usual sleep/retry loop — the call returns as soon as the request arrives (or empty after the timeout).
🤖 AI agent tool-callback
Testing that your agent's tool actually calls back the URL you gave it? Give it a Catchr bin and inspect the real request it made — headers, auth token, body and all.
bin = requests.post("https://catchr.perch-app.workers.dev/api/bins").json()
agent.run(tools=[Webhook(url=bin["url"])], task="notify me when done")
reqs = requests.get(f'https://catchr.perch-app.workers.dev/api/bins/{bin["id"]}/requests',
params={"token": bin["token"]}).json()["requests"]
print(reqs[0]["headers"]) # did it send the auth header you expected?
print(reqs[0]["body"]) # did it send the payload you expected?
🧪 Node (Vitest / Jest)
const BASE = "https://catchr.perch-app.workers.dev";
test("stripe webhook is forwarded", async () => {
const bin = await (await fetch(BASE + "/api/bins", {method:"POST"})).json();
await triggerCheckout(bin.url); // your code under test
const { requests } = await (await fetch(
`${BASE}/api/bins/${bin.id}/requests?token=${bin.token}`)).json();
expect(requests.length).toBeGreaterThan(0);
expect(JSON.parse(requests[0].body).type).toBe("checkout.session.completed");
});
⚙️ GitHub Actions step
- name: Verify webhook delivery
run: |
BIN=$(curl -s -X POST https://catchr.perch-app.workers.dev/api/bins)
ID=$(echo "$BIN" | jq -r .id); TOK=$(echo "$BIN" | jq -r .token)
URL=$(echo "$BIN" | jq -r .url)
./scripts/deploy.sh --notify-url "$URL"
# long-poll up to 20s instead of a fixed sleep — returns the moment it lands
N=$(curl -s "https://catchr.perch-app.workers.dev/api/bins/$ID/requests?token=$TOK&wait=20" | jq '.count')
test "$N" -ge 1 || { echo "no webhook fired"; exit 1; }
More endpoints in the API docs. Machine-readable: /openapi.json · /llms.txt.