# webhook-tester — Agent Runbook

Base URL: `https://webhook-debugger.dev`

**Set yourself up:** run `curl -sX POST https://webhook-debugger.dev/api/sessions`, save the returned `id` + `webhook_url`, send test requests to `webhook_url`, then poll `GET https://webhook-debugger.dev/api/sessions/<id>/requests` and `GET https://webhook-debugger.dev/api/requests/<id>` to inspect. Persist important requests past the 24h prune with `POST https://webhook-debugger.dev/api/requests/<request-id>/share`. That is the whole loop — the STEPs below spell it out, do them in order.

This tool captures HTTP requests so you can inspect them. There are **no accounts and no auth**: a session is identified by an unguessable UUID that doubles as a bearer token — save it, reuse it in every later call, and keep it secret. Sessions and unshared requests expire after 24 hours.

## Do this now — the runbook

### STEP 1 — Create your session FIRST (you have none yet)

You do not have a session. Create one before anything else — this is always your first action:

```bash
curl -sX POST https://webhook-debugger.dev/api/sessions
```

Returns `201`:

```json
{
  "id": "<uuid>",
  "webhook_url": "https://webhook-debugger.dev/webhook/<uuid>",
  "dashboard_url": "https://webhook-debugger.dev/d/<uuid>",
  "api_url": "https://webhook-debugger.dev/api/sessions/<uuid>",
  "expires_at": "...",
  "created_at": "...",
  "links": { "self": ".../api/sessions/<uuid>", "requests": ".../api/sessions/<uuid>/requests" }
}
```

**Save the `id` — it is your handle, reused in every later call.** `webhook_url` is where you send test requests.

### STEP 2 — Send test requests to your webhook_url

```bash
curl -X POST '<webhook_url>' \
  -H 'Content-Type: application/json' \
  -d '{"hello":"world"}'
```

Any method and any body work — point a service's webhook config, a script, or curl at `webhook_url`.

### STEP 3 — List what arrived (newest-first)

```bash
curl -s 'https://webhook-debugger.dev/api/sessions/<id>/requests'
```

Poll for just-new arrivals by passing the previous response's `meta.latest_id` as `?after=<last request id>`.

### STEP 4 — Inspect one request fully

```bash
curl -s 'https://webhook-debugger.dev/api/requests/<request-id>'
```

Returns headers, the parsed body, and the exact `raw_body`.

### STEP 5 — Persist an important request past the 24h prune

```bash
curl -sX POST 'https://webhook-debugger.dev/api/requests/<request-id>/share'
```

Returns the same WebhookRequest with `shared_at` set and a `share_url`.

A human can watch the same session live in a browser at `https://webhook-debugger.dev/d/<id>`.

---

## Endpoint reference

All responses are JSON. The machine-readable index is at `GET https://webhook-debugger.dev/api`.

### POST /api/sessions

Create a session. No request body needed. Returns `201` with the shape shown in STEP 1 above.

### GET /api/sessions/{id}

Session metadata plus a `request_count`. Returns `404` for an unknown/malformed id.

### GET /api/sessions/{id}/requests

List the session's requests, **newest-first**.

Query parameters:
- `after=<request_id>` — polling cursor. Returns only requests created **after**
  the given request (by `created_at`). Pass the `latest_id` from your previous
  response to fetch just the new arrivals. An unknown `after` is ignored.
- `limit=<1-100>` — page size, default `50` (clamped to the 1–100 range).

Response:

```json
{
  "data": [ /* WebhookRequest objects, newest first */ ],
  "meta": { "count": 3, "has_more": false, "latest_id": "<uuid|null>" }
}
```

To stream new requests: poll this endpoint, then on the next call pass
`?after={meta.latest_id}` (only update your cursor when `count > 0`).

### GET /api/requests/{id}

A single request in full. Returns `404` for an unknown/malformed id.

### POST /api/requests/{id}/share

Persist a request past the 24h prune. This is idempotent: calling it again keeps
the original `shared_at`. Returns the same WebhookRequest shape with `shared_at`
and `share_url`.

## The WebhookRequest shape

```json
{
  "id": "<uuid>",
  "session_id": "<uuid>",
  "method": "POST",
  "headers": { "content-type": ["application/json"] },
  "content_type": "application/json",
  "body": { "hello": "world" },
  "raw_body": "{\"hello\":\"world\"}",
  "size_bytes": 17,
  "hostname": "webhook-tester.test",
  "user_agent": "curl/8.4.0",
  "shared_at": null,
  "created_at": "2026-06-05T20:00:00.000000Z",
  "web_url": "https://webhook-debugger.dev/d/<session-uuid>",
  "share_url": "https://webhook-debugger.dev/shared-webhook/<request-uuid>"
}
```

Field notes:
- **`headers`** — keys are lowercased and **every value is an array of strings**
  (a header can legally repeat), e.g. `{"content-type":["application/json"]}`.
  `content_type` is a convenience copy of the first `content-type` value.
- **`body`** — the *parsed* form/JSON input. It is `{}` (empty) for bodies that
  aren't form-encoded or JSON (e.g. XML, plain text, binary).
- **`raw_body`** — the exact raw request body as a string (may be `null`). Use
  this for XML/plain-text/binary payloads where `body` is empty.
- **`web_url`** — the human dashboard for the session this request belongs to.
- **`share_url`** — the human-readable shared request URL. Call
  `POST /api/requests/{id}/share` to set `shared_at` and keep the request past
  the 24h prune.

## Rate limits

- `POST /api/sessions` and `POST /api/requests/{id}/share` — **30 requests/minute** per IP.
- All reads (`GET /api`, sessions, requests) — **120 requests/minute** per IP,
  generous enough for polling with `?after=`.

Exceeding a limit returns HTTP `429`.

## A complete polling loop (bash)

```bash
BASE=https://webhook-debugger.dev
SID=$(curl -s -X POST "$BASE/api/sessions" | jq -r .id)
echo "Send your webhooks to: $BASE/webhook/$SID"

AFTER=""
while true; do
  URL="$BASE/api/sessions/$SID/requests"
  [ -n "$AFTER" ] && URL="$URL?after=$AFTER"
  RESP=$(curl -s "$URL")
  echo "$RESP" | jq -c '.data[] | {method, content_type, body, raw_body}'
  NEW=$(echo "$RESP" | jq -r '.meta.latest_id // empty')
  [ -n "$NEW" ] && AFTER="$NEW"
  sleep 2
done
```
