# llms.txt and agents Every Pixelhooks doc page is plain markdown, and the whole site is indexed in two artifacts at the site root so an AI assistant can read the documentation directly rather than scraping HTML: - [`/llms.txt`](https://hooks.pixelhop.io/llms.txt){rel=""nofollow""} — a curated index of every doc page, with links and one-line descriptions. - [`/llms-full.txt`](https://hooks.pixelhop.io/llms-full.txt){rel=""nofollow""} — the full text of every page concatenated into a single file. These follow the [llms.txt convention](https://llmstxt.org){rel=""nofollow""} and are generated at build time, so they always reflect the current docs. ## Any page as markdown Every doc page is also available as raw markdown — just **append `.md` to its URL**. For example, this page is at [`/docs/agents/llms-and-agents.md`](https://hooks.pixelhop.io/docs/agents/llms-and-agents.md), and the MCP setup guide is at `/docs/agents/mcp-setup.md`. The links in `/llms.txt` point straight at these `.md` files, so an agent can fetch exactly the page it needs without parsing HTML. Every doc page also has a **"Copy page"** menu (top-right) that copies the markdown to your clipboard, copies an agent-fetchable link, opens the raw markdown, or launches the page directly in ChatGPT or Claude. ## Using them Point your assistant at `/llms.txt` to discover what exists, then have it fetch the specific page it needs. For a one-shot grounding, feed it `/llms-full.txt` — the entire documentation in one request. ```bash curl https://hooks.pixelhop.io/llms.txt ``` ::callout{type="info"} If you want an agent to *act* on Pixelhooks — create bins, inspect requests — use the [MCP server](https://hooks.pixelhop.io/docs/agents/mcp-setup) instead. `llms.txt` is for *reading* the docs; MCP is for *driving* the service. :: ## What's next - [MCP setup](https://hooks.pixelhop.io/docs/agents/mcp-setup) — give an agent the tools to create bins and wait for requests. # MCP setup Pixelhooks ships an [MCP](https://modelcontextprotocol.io){rel=""nofollow""} server, `@pixelhooks/mcp`, that lets an AI agent create bins and inspect captured requests directly — no browser, no manual `curl`. It runs locally over stdio and talks to the public Pixelhooks API, so there's nothing to host. The headline tool is `wait_for_request`: point a service at a bin, then ask the agent to wait, and it blocks until the next request lands and hands back the full detail. ## Install The server runs via `npx`, so there's no global install. Add it to your client's MCP configuration. ::callout{type="info"} The server talks to the hosted Pixelhooks API out of the box — there's nothing to host or configure. :: ### Claude Code ```bash claude mcp add pixelhooks -- npx -y @pixelhooks/mcp ``` Or add it to `.mcp.json` in your project: ```json { "mcpServers": { "pixelhooks": { "command": "npx", "args": ["-y", "@pixelhooks/mcp"] } } } ``` ### Claude Desktop Edit `claude_desktop_config.json` (on macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`): ```json { "mcpServers": { "pixelhooks": { "command": "npx", "args": ["-y", "@pixelhooks/mcp"] } } } ``` ### Cursor Add the same `mcpServers` block to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project), then restart Cursor. ## Tools | Tool | Arguments | Returns | | ----------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `create_bin` | — | New bin id, its capture URL, and a human view URL. | | `get_bin` | `binId` | Bin metadata (confirms it exists). | | `list_requests` | `binId` | Captured request summaries, newest first. | | `get_request` | `binId`, `requestId` | One request in full: method, headers, query, path, body. | | `wait_for_request` | `binId`, `timeoutMs?` | Blocks until the next request arrives, then returns it. Bounded (default 25s); returns a "no request yet" result on timeout. | | `list_my_bins` | `cursor?` | Your account's bins (newest first) with capture/view URLs. Up to 500 per call; when `hasMore` is set, pass the returned `nextCursor` back as `cursor` for the next page. | | `delete_bin` | `binId` | Removes a bin from your account — an unlink, not a destructive delete, so captured data persists. | | `rename_bin` | `binId`, `label?` | Sets or clears a bin's label. Omit or empty `label` to clear it; max 255 characters. | | `get_response_config` | `binId` | Reads a bin's configured capture response (`{ configured, config }`). | | `set_response_config` | `binId`, `status?`, `contentType?`, `body?`, `headers?`, `delayMs?` | Configures the response the capture endpoint serves — a **full replacement** (omitted fields revert to defaults). | | `clear_response_config` | `binId` | Clears a bin's response config, reverting the capture endpoint to `200 OK`. | `list_my_bins`, `delete_bin`, `rename_bin`, `get_response_config`, `set_response_config`, and `clear_response_config` are account-scoped: set `PIXELHOOKS_API_TOKEN` (a personal access token) in the server config to use them. Without a token they return an actionable error rather than acting anonymously. ## A typical loop A debugging session usually looks like this: 1. **`create_bin`** → the agent gets a capture URL like `https://hooks.pixelhop.io/h/abc123cuid`. 2. The agent configures the service under test (or asks you to) to send to that URL. 3. **`wait_for_request`** with the `binId` → the agent blocks until the webhook fires, then receives the method, headers, and body of exactly what was sent. 4. If nothing arrives within the timeout, the tool returns a "no request yet" result and the agent can simply call it again. ::callout{type="tip"} The bin id is the only key to its requests — treat capture URLs as secrets and don't paste them where you wouldn't paste a token. :: ## What's next - [llms.txt and agents](https://hooks.pixelhop.io/docs/agents/llms-and-agents) — let an agent read these docs directly. - [REST API reference](https://hooks.pixelhop.io/docs/developers/rest-api) — what each tool calls under the hood. # Quickstart Pixelhooks gives you a throwaway URL that records every HTTP request sent to it — method, headers, body, query string, and path — so you can see exactly what a webhook or callback is sending before you build the receiving end. No account, no setup, no local server. ::callout{type="info"} Every bin is anonymous. The bin id is an unguessable cuid and acts as the only key — anyone with the URL can see the requests, so treat it like a secret. :: ## 1. Create a bin Make a `POST` to `/api/bins`. You get back a bin `id` and its creation time. ```bash curl -X POST https://hooks.pixelhop.io/api/bins # { "id": "abc123cuid", "createdAt": "2026-06-19T09:00:00.000Z" } ``` Your **capture URL** is the origin plus `/h/` and the bin id: ```text https://hooks.pixelhop.io/h/abc123cuid ``` ## 2. Send something to it Point any webhook, script, or `curl` at the capture URL. Every HTTP method works, and so does any sub-path beneath the bin — `/abc123cuid/webhooks/stripe` is recorded against the same bin, with the path preserved. ```bash curl -X POST https://hooks.pixelhop.io/h/abc123cuid/webhooks/stripe \ -H 'content-type: application/json' \ -d '{"event":"payment_intent.succeeded"}' # OK ``` The capture endpoint always replies `200 OK` — it never rejects a request, so the sender behaves exactly as it would against your real endpoint. ## 3. Read it back List the bin's requests, newest first: ```bash curl https://hooks.pixelhop.io/api/bins/abc123cuid/requests ``` ```json [ { "id": "req_1", "method": "POST", "path": "/webhooks/stripe", "query": "", "contentType": "application/json", "bodySize": 41, "truncated": false, "createdAt": "2026-06-19T09:00:05.000Z" } ] ``` Then fetch one request in full — headers, query, and the decoded body: ```bash curl https://hooks.pixelhop.io/api/bins/abc123cuid/requests/req_1 ``` That is the whole loop: create a bin, point a service at it, read what arrived. ## What's next - The [REST API reference](https://hooks.pixelhop.io/docs/developers/rest-api) documents every endpoint, the response shapes, and the retention and body-size limits. - Driving this from an AI agent? The [MCP setup guide](https://hooks.pixelhop.io/docs/agents/mcp-setup) wires Pixelhooks into your agent so it can create bins and wait for requests without leaving the conversation. - Want your bins to stick around? Sign-in is **optional** — anonymous use stays the default — but if you [create an account](https://hooks.pixelhop.io/sign-in) with an email and password (or GitHub/Google where configured), the bins you make are saved to a list you can come back to. # REST API reference The Pixelhooks API has two surfaces: the **capture endpoint** under the `/h/` prefix, where requests are recorded, and the **management API** under `/api`, where you read bins and their captured requests. Capturing and reading are anonymous — the bin id is the only credential. A small **account-scoped** surface (listing, labelling, and removing the bins you own) sits behind authentication — see [Authenticated endpoints](https://hooks.pixelhop.io/#authenticated-endpoints). All examples use `https://hooks.pixelhop.io`, the hosted Pixelhooks origin. The app, the management API, and the capture endpoint all share this single origin, so there are no cross-origin cookies and no CORS to worry about. ## Capture endpoint ```text ANY /h/:binId ANY /h/:binId/* ``` Records the incoming request against the bin and, by default, responds `200 OK` with the body `OK`. Accepts any HTTP method and any sub-path beneath the bin id; the sub-path is stored as the request's `path` (the bare bin URL records `/`). The bin's owner can change what this endpoint returns — status, headers, body, content-type, and an artificial delay — see [Configure the response](https://hooks.pixelhop.io/#configure-the-response). For each request Pixelhooks stores the method, request headers, raw body, query string, the path that was hit, and a received-at timestamp. ::callout{type="info"} Requests to a bin id that doesn't exist also return `200 OK` — nothing is stored, and the response doesn't reveal whether the bin exists. :: **Limits** - Each bin keeps only its **most recent 100 requests**. When a new request would exceed the cap, the oldest is dropped. - Bodies are captured up to **1 MB**. Larger bodies are truncated to the cap and flagged with `truncated: true`; the request is never rejected. - **Anonymous bins idle-expire after 30 days.** A bin that goes 30 days without receiving a request is purged automatically — its captured requests and any configured response are deleted, and the bin id stops resolving (capture and read both behave as if it never existed). Each captured request resets the countdown. Bins created while signed in are claimed by your account and **never auto-expire**. ## Create a bin ```text POST /api/bins ``` ```json { "id": "abc123cuid", "createdAt": "2026-06-19T09:00:00.000Z" } ``` The `id` doubles as the bin's capture token. The capture URL is the origin plus `/h/` and the id: `https://hooks.pixelhop.io/h/abc123cuid`. ## Get bin metadata ```text GET /api/bins/:binId ``` Returns `{ "id", "createdAt" }`, or `404` if the bin doesn't exist. ## List captured requests ```text GET /api/bins/:binId/requests ``` Returns lightweight summaries, **newest first**. Use `get request` for full detail. ```json [ { "id": "req_1", "method": "POST", "path": "/webhooks/stripe", "query": "attempt=2", "contentType": "application/json", "bodySize": 41, "truncated": false, "createdAt": "2026-06-19T09:00:05.000Z" } ] ``` ## Get one request ```text GET /api/bins/:binId/requests/:requestId ``` Returns the full request. Text-like bodies (text, JSON, form, XML, or anything that decodes as valid UTF-8) are returned decoded in `bodyText`. Binary bodies return `isText: false` and `bodyText: null` — fetch the bytes from the raw endpoint instead. ```json { "id": "req_1", "binId": "abc123cuid", "method": "POST", "path": "/webhooks/stripe", "query": "?attempt=2", "headers": { "content-type": "application/json", "user-agent": "Stripe/1.0" }, "contentType": "application/json", "bodySize": 41, "truncated": false, "ip": "203.0.113.10", "kind": "json", "isText": true, "bodyText": "{\"event\":\"payment_intent.succeeded\"}", "fields": null, "hasBody": true, "createdAt": "2026-06-19T09:00:05.000Z" } ``` Beyond the raw body, the detail response **categorizes** the body and, for the two key/value shapes, **parses** it into fields: - `kind` — a coarse body category, chosen from the content-type and falling back to byte-sniffing when it's unknown. One of: `json`, `form` (`application/x-www-form-urlencoded`), `multipart` (`multipart/form-data`), `xml`, `graphql`, `sparql`, `text`, `binary`, or `none` (empty/absent body). - `fields` — parsed key/value entries for `form` and `multipart` bodies only; `null` for every other `kind`. Each entry is `{ "name", "value" }` for a plain field, or `{ "name", "filename", "contentType", "size" }` for a file part (the bytes themselves are not embedded — use the raw endpoint). Values are decoded UTF-8, capped at 8 KB each. These two fields are **detail-only**; the [list endpoint](https://hooks.pixelhop.io/#list-captured-requests) summary is unchanged. ## Download a raw body ```text GET /api/bins/:binId/requests/:requestId/raw ``` Returns the stored body bytes as `application/octet-stream` (forced, regardless of the captured content-type, so an attacker-controlled HTML/JS body can't execute). `404` if the request has no body. ## Live stream ```text GET /api/bins/:binId/stream ``` A [Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events){rel=""nofollow""} stream of the bin's captured requests. Each new request is delivered as a default `message` event whose `data` is the request summary (same shape as the list endpoint). A named `ping` event is sent periodically as a heartbeat and can be ignored. Returns `404` before the stream opens if the bin doesn't exist. ```js const es = new EventSource("https://hooks.pixelhop.io/api/bins/abc123cuid/stream"); es.onmessage = (e) => console.log("captured:", JSON.parse(e.data)); ``` ## Configure the response By default the capture endpoint returns `200 OK`. A bin's **owner** can configure a custom response so the endpoint can drive client logic that branches on it — retry on `5xx`, follow a redirect, parse a JSON ack, or test timeout handling. These three endpoints are **owner-only**: they require the session cookie or an `x-api-key` personal access token of the user who owns the bin (see [Authenticated endpoints](https://hooks.pixelhop.io/#authenticated-endpoints)), and return `404` for anyone else so a bin's configuration never leaks. Reading and serving the configured response over `/h/:binId` stays fully public. ```text GET /api/bins/:binId/response PUT /api/bins/:binId/response DELETE /api/bins/:binId/response ``` `GET` returns `{ "configured": boolean, "config": { ... } }`. `PUT` upserts the config (full replacement). `DELETE` clears it, reverting the endpoint to `200 OK`. ```json PUT /api/bins/abc123cuid/response { "status": 503, "contentType": "application/json", "body": "{\"error\":\"try again\"}", "headers": { "Retry-After": "5" }, "delayMs": 250 } ``` **Fields** - `status` — integer `200`–`599`. For `204`, `205`, and `304` the body and content-type are dropped (those statuses forbid a body). - `contentType` — defaults to `text/plain; charset=utf-8`. **HTML-ish types (`text/html`, `image/svg+xml`, `application/xhtml+xml`) are rejected**, not downgraded — the capture endpoint shares an origin with the app, so a rendered HTML body would be script execution against that origin. Every configured response is also sent with `X-Content-Type-Options: nosniff` and `Content-Security-Policy: sandbox`. - `body` — a UTF-8 string, up to **64 KB**. - `headers` — an allowlist of representational/caching headers (`Cache-Control`, `Content-Disposition`, `Content-Language`, `ETag`, `Expires`, `Last-Modified`, `Vary`, `Retry-After`, `Link`, `WWW-Authenticate`, `Age`, `Location`) plus any custom `X-*` header. `Location` must be an absolute `http(s)` URL or a `/` path. Cookies, CORS, security/CSP/framing, hop-by-hop, and runtime-managed headers are rejected. Up to 20 headers. - `delayMs` — artificial delay before responding, `0`–**10000** (10s). Invalid config is rejected with `400` and a message explaining why. ::callout{type="info"} You can also set this from the bin's page in the app (the **Configure response** panel, visible only to the owner). :: ## Authenticated endpoints Creating, capturing, and reading bins needs no account. But if you [sign in](https://hooks.pixelhop.io/sign-in), the bins you create are saved to your account, and a small set of endpoints lets you manage that list. These are the only authenticated endpoints — everything above is anonymous. **Authentication.** Authenticate with *either* the better-auth **session cookie** (set automatically in the browser after sign-in) *or* an `x-api-key` header carrying a **personal access token**: ```text x-api-key: ph_xxxxxxxx… ``` Because the app, the API, and the capture endpoint share one origin, there are no cross-origin cookies and no CORS; the session cookie is `SameSite=Lax`. A request with no valid credential gets `401`. A bin that exists but isn't yours returns `404`, never `403` — ownership never leaks. **Personal access tokens.** Mint a PAT from the app once signed in. The raw `ph_…` token is shown **exactly once** at creation and is never retrievable again; it carries a \~1-year expiry. PATs are managed through `GET`/`POST /api/pat` and `DELETE /api/pat/:id`, which are **browser-session-only** (you can't mint a PAT with a PAT), so they're driven from the app rather than scripted. ### List your bins ```text GET /api/bins?limit=&cursor= ``` Returns the bins you own, **newest first**, metadata only (no per-bin request counts). Keyset (cursor) paginated. Two optional query params: - `limit` — page size, an integer `1`–`500` (default `500`). Out of range → `400`. - `cursor` — an opaque pagination cursor; pass back the `nextCursor` from a previous response to fetch the next page. Treat it as opaque (don't construct or parse it). Malformed → `400`. `hasMore` is `true` when more bins exist beyond this page; when it is, the response also includes `nextCursor`. This backs the MCP [`list_my_bins`](https://hooks.pixelhop.io/docs/agents/mcp-setup) tool. ```json { "bins": [ { "binId": "abc123cuid", "label": "Stripe staging", "createdAt": 1750323600000 } ], "hasMore": true, "nextCursor": "MTc1MDMyMzYwMDAwMDphYmMxMjNjdWlk" } ``` ### Label a bin ```text PATCH /api/bins/:binId ``` Sets or clears a bin's label. Body is `{ "label": "…" }`; an empty string, `null`, or an omitted `label` clears it. Max 255 characters. Returns `{ "ok": true }`, or `404` if the bin isn't yours. Backs the MCP [`rename_bin`](https://hooks.pixelhop.io/docs/agents/mcp-setup) tool. ```bash curl -X PATCH https://hooks.pixelhop.io/api/bins/abc123cuid \ -H 'x-api-key: ph_xxxxxxxx' \ -H 'content-type: application/json' \ -d '{"label":"Stripe staging"}' # { "ok": true } ``` ### Remove a bin ```text DELETE /api/bins/:binId ``` Removes the bin from your list. This is an **unlink, not a destructive delete** — the bin and its captured requests stay readable by id; only the ownership link is dropped. Because the bin is now unowned, it reverts to the **30-day anonymous idle expiry** (an owned bin never auto-expires), so an unlinked bin left unused is eventually purged. Returns `{ "ok": true }`, or `404` if the bin isn't yours. Backs the MCP [`delete_bin`](https://hooks.pixelhop.io/docs/agents/mcp-setup) tool. ## What's next - [Quickstart](https://hooks.pixelhop.io/docs/developers/quickstart) — the create → send → read loop. - [MCP setup](https://hooks.pixelhop.io/docs/agents/mcp-setup) — drive all of the above from an AI agent.