persistent api test agent
PURL is an agentic API testing tool. You write a plain-English user story describing how your API should behave, and PURL takes it from there. It generates test cases, executes them against a live endpoint, and streams the results back in real time. If something goes wrong mid-run, it reanalyzes and self-corrects on the next case.
It sits one layer above tools like Postman and Bruno. Those tools require you to define every request and assertion manually. PURL infers what to test from a natural language description and runs the full cycle on its own.
v1 targets solo developers testing their own APIs locally during active development. It is not a hosted service or a team tool.
PURL implements a genuine agentic loop, not a one-shot prompt. The reasoning layer and the execution layer are strictly separated.
user story
|
v
claude plans test cases
|
v
tool_use: http_request --> fastapi executes HTTP call
| |
v v
tool_result: raw response <--------
|
v
claude evaluates: passed / failed / error
|
v
SSE stream --> frontend card resolves
|
v
repeat until generate_report
A few things worth calling out:
- Self-correction: if a request returns an unexpected shape (wrong field name, missing required field), Claude observes the response, adjusts, and retries with a corrected request on the next case. You can watch this happen in real time.
- Token carry-forward: if a test case calls a login endpoint and gets back a bearer token, PURL extracts it automatically and injects it into all subsequent authenticated requests. No manual step.
- Automatic re-auth: if a token expires mid-run and a protected endpoint returns 401, PURL silently re-authenticates, gets a fresh token, and resumes from the failed case. The re-auth banner appears inline in the stream right where it happened.
- OpenAPI schema fetch: at run start PURL attempts
GET {baseURL}/openapi.json. If it finds a schema, it passes it to Claude as context for more precise test case generation. If not, Claude works from the story alone.
| layer | tech |
|---|---|
| backend | FastAPI, Python |
| frontend | React, TypeScript |
| AI | Anthropic Claude (tool use API) |
| HTTP client | httpx (async) |
| streaming | Server-Sent Events (SSE) |
purl/
backend/
agent/
loop.py # core agentic loop -- tool_use / tool_result cycle
prompts.py # system prompt and run prompt builders
tools.py # tool definitions passed to Claude
execution/
http_request.py # fires actual HTTP calls, measures latency
auth_handler.py # re-auth and token extraction
schema_fetch.py # OpenAPI schema retrieval
streaming/
sse.py # SSE event formatters
models/
schemas.py # all shared data types
routers/
run.py # /run SSE endpoint
auth.py # /get-token endpoint
config.py
main.py
frontend/
src/
components/
InputPanel/ # user story, base URL, auth, slider, run button
OutputPanel/ # streaming terminal, test cards, final report
TestCard/ # individual test case card
hooks/
useRun.ts # SSE consumer, run state
useAuth.ts # Get Token flow
types/
run.ts # frontend type definitions
- Python 3.11+
- Node.js 18+
- An Anthropic API key
cd backend
python -m venv venv
source venv/bin/activate # windows: venv\Scripts\activate
pip install -r requirements.txtCreate a .env file in backend/:
# Anthropic
ANTHROPIC_API_KEY=your_key_here
SONNET_MODEL=claude-sonnet-4-6
HAIKU_MODEL=claude-haiku-4-5-20251001
# Timeouts (seconds)
REQUEST_TIMEOUT_SECONDS=10
SCHEMA_TIMEOUT_SECONDS=3
AUTH_TIMEOUT_SECONDS=10
# Rate limiting
RATE_LIMIT=5/minute
# CORS Origins
CORS_ORIGINS=["http://localhost:5173","http://localhost:3000"]Start the backend:
uvicorn main:app --reloadBackend runs on http://localhost:8000.
cd frontend
npm install
npm run devFrontend runs on http://localhost:5173.
- Open
http://localhost:5173 - Write a plain-English user story in the input panel
- Set your base URL (localhost is fully supported)
- If your API requires auth, expand the authentication section and fill in your credentials, then click Get Token to auto-fill the bearer field
- Adjust the test case slider (default 8, max 15)
- Click Run PURL
Results stream in as each test case resolves. The final report shows total, passed, failed, and errored with the total run time.
PURL supports Bearer token APIs. The auth section takes:
| field | description |
|---|---|
| auth endpoint | login path, e.g. /auth/login |
| email / username | identifier sent in the login request body |
| password | password sent in the login request body |
| credential field | the JSON field name for the identifier (default: email) |
| token path | dot-notation path to extract the token from the login response (default: access_token) |
| bearer token | auto-filled by Get Token, manually editable as fallback |
The quality of the test cases depends heavily on the story. A few things that help:
- Focus on one feature per run. A story about login will generate sharper cases than a story about the whole API.
- State expected behavior explicitly. "Returns 200 with an access token" gives Claude something concrete to evaluate against.
- Include failure modes. "Invalid credentials should return 401" tells PURL to cover the unhappy path too.
A story that works well:
A user can register a new account and login to receive a bearer token. Valid registration returns 200. Duplicate accounts are rejected with 400. Logging in with valid credentials returns 200 with an access token that grants access to protected endpoints. Invalid credentials are rejected with 401.
- Bearer token APIs only: cookie-based auth is not supported in v1
- No run history: runs are impermanent, results are not persisted
- Rate limits: large prompts or long runs may hit Anthropic's input token rate limit. If this happens, wait 60 seconds and retry. Automatic retry handling is deferred to v2.
- Test case cap: runs are capped at 15 test cases to keep latency and API cost bounded
- PostgreSQL run history and history UI
- Recently used stories as preset chips (replaces hardcoded presets, powered by run history)
- Expandable request detail per test case: method, URL, headers, and body that were sent
- Cookie-based auth support
- Export run as Bruno / Postman collection
- Public hosting with per-session API key
- Automatic rate limit retry
- SSRF protection (deferred until public hosting)
- Raise or remove the 15-case cap based on real-world run time data
Setting up Postman and Bruno collections takes time, defining every request, writing every assertion, keeping it all in sync as your API changes. Sometimes you just want to point at an endpoint and know if it works. PURL is that shortcut.
purl reads, infers, executes, evaluates -- while you work.
