Internal REST API
When Probe starts, it spins up an internal HTTP server on 127.0.0.1:<random> so probe-mcp and other local processes can read traffic, manage rules, and replay requests. The same API is documented here in case you want to script against Probe from a shell, a CI job, or your own tooling.
The server is implemented with axum and lives next to the proxy in the Rust crate.
Discovery — handshake file
Section titled “Discovery — handshake file”Probe writes the listening port and a freshly-generated bearer token to:
~/.probe/api-handshake.json (macOS)%USERPROFILE%\.probe\api-handshake.json (Windows)Shape:
{ "port": 51827, "token": "f3b1...c2a9", "pid": 73114, "version": "1.4.0", "started_at": 1731081822}- The file is written with mode
0600on Unix. - A fresh token is generated each Probe launch — restarts invalidate previous tokens.
- The file is removed when Probe quits cleanly.
Read it once, point your client at http://127.0.0.1:<port>/api/v1/..., send the token in the Authorization header.
Authentication
Section titled “Authentication”Every endpoint except /api/v1/health requires:
Authorization: Bearer <token>Without the header (or with the wrong token), the API returns 401 Unauthorized.
Quick check:
PORT=$(jq -r .port ~/.probe/api-handshake.json)TOKEN=$(jq -r .token ~/.probe/api-handshake.json)curl -s "http://127.0.0.1:$PORT/api/v1/health"# {"status":"ok","version":"1.4.0","capturing":true,"entry_count":42, ...}Health
Section titled “Health”GET /api/v1/health
Section titled “GET /api/v1/health”Public — no auth required. Useful for liveness checks.
{ "status": "ok", "version": "1.4.0", "capturing": true, "entry_count": 142, "uptime_secs": 3621, "api_port": 51827}Traffic
Section titled “Traffic”GET /api/v1/traffic
Section titled “GET /api/v1/traffic”List captured entries with optional filters.
| Query param | Type | Description |
|---|---|---|
limit | int | Default 50, capped at 500. |
offset | int | Default 0. |
host | string | Substring match against host. |
method | string | Exact match. |
status | int | Exact status code (sets both min and max). |
status_min / status_max | int | Status range. |
path | string | URL path filter. |
search | string | Substring search across URL and body. |
has_error | bool | true for status ≥ 400 only. |
since_id | int | Only entries with id > since_id. |
since | int | Only entries with timestamp >= since (ms since epoch). |
curl -s "http://127.0.0.1:$PORT/api/v1/traffic?host=api.example.com&limit=10" \ -H "Authorization: Bearer $TOKEN"Response:
{ "items": [ { "id": 142, "method": "POST", "url": "https://api.example.com/v1/users", "host": "api.example.com", "status_code": 201, "content_type": "application/json", "duration_ms": 184, "timestamp": 1731081900123, "client_addr": "Chrome", "has_response": true } ], "total": 142, "offset": 0, "limit": 10}GET /api/v1/traffic/{id}
Section titled “GET /api/v1/traffic/{id}”Full detail of one entry — including request/response headers and bodies.
{ "id": 142, "method": "POST", "url": "https://api.example.com/v1/users", "host": "api.example.com", "status_code": 201, "timestamp": 1731081900123, "req_headers": [["content-type", "application/json"], ["authorization", "Bearer ..."]], "req_body": "{\"name\":\"Ada\"}", "res_headers": [["content-type", "application/json"]], "res_body": "{\"id\":1,\"name\":\"Ada\"}", "content_type": "application/json", "client_addr": "Chrome", "duration_ms": 184, "has_response": true}404 if the id doesn’t exist.
DELETE /api/v1/traffic
Section titled “DELETE /api/v1/traffic”Drop every entry. Returns 204 No Content.
DELETE /api/v1/traffic/{id}
Section titled “DELETE /api/v1/traffic/{id}”Drop a single entry. 204 on success, 404 if missing.
GET /api/v1/traffic/{id}/export/curl
Section titled “GET /api/v1/traffic/{id}/export/curl”Plain text — a runnable cURL command. Content-Type: text/plain.
GET /api/v1/traffic/{id}/export/raw
Section titled “GET /api/v1/traffic/{id}/export/raw”Plain text — raw HTTP request and response.
POST /api/v1/traffic/{id}/replay
Section titled “POST /api/v1/traffic/{id}/replay”Replay a captured request. Body is {} (no parameters today). Response carries the new entry’s status, headers, and body. The replay also lands in the live traffic list as a new entry.
GET /api/v1/traffic/{id1}/diff/{id2}
Section titled “GET /api/v1/traffic/{id1}/diff/{id2}”Compare two entries. Returns a structured diff over method, URL, status, headers, and body.
Capture control
Section titled “Capture control”GET /api/v1/capture/status
Section titled “GET /api/v1/capture/status”{ "capturing": true, "entry_count": 142 }POST /api/v1/capture/start
Section titled “POST /api/v1/capture/start”Enable recording. Returns 204.
POST /api/v1/capture/stop
Section titled “POST /api/v1/capture/stop”Disable recording — traffic still proxies, it just isn’t stored. Returns 204.
Breakpoints
Section titled “Breakpoints”GET /api/v1/breakpoints
Section titled “GET /api/v1/breakpoints”Array of {pattern, intercept_request, intercept_response}.
PUT /api/v1/breakpoints
Section titled “PUT /api/v1/breakpoints”Create or update a breakpoint. Pattern goes in the body so URL-shaped patterns (with slashes) don’t need re-encoding.
{ "pattern": "api.example.com/users/*", "intercept_request": true, "intercept_response": true }204 No Content.
POST /api/v1/breakpoints/delete
Section titled “POST /api/v1/breakpoints/delete”Delete a single breakpoint by pattern.
{ "pattern": "api.example.com/users/*" }DELETE /api/v1/breakpoints
Section titled “DELETE /api/v1/breakpoints”Drop every breakpoint. 204.
Intercepts
Section titled “Intercepts”A breakpoint or intercept-host match parks the request until you resume it.
GET /api/v1/intercepts
Section titled “GET /api/v1/intercepts”Array of {id} — one per paused request.
POST /api/v1/intercepts/{id}/resume
Section titled “POST /api/v1/intercepts/{id}/resume”Release a paused request, optionally rewriting it.
{ "abort": false, "status_code": 200, "headers": [["x-debug", "1"]], "body": "{\"forced\":true}", "url": "https://staging.example.com/v1/users"}204 on success, 404 if the id no longer exists.
Ignored hosts
Section titled “Ignored hosts”GET /api/v1/ignored-hosts
Section titled “GET /api/v1/ignored-hosts”Array of host strings.
PUT /api/v1/ignored-hosts/{host}
Section titled “PUT /api/v1/ignored-hosts/{host}”Add a host. 204.
DELETE /api/v1/ignored-hosts/{host}
Section titled “DELETE /api/v1/ignored-hosts/{host}”Remove a host. 204.
DELETE /api/v1/ignored-hosts
Section titled “DELETE /api/v1/ignored-hosts”Clear the list. 204.
Ignored clients
Section titled “Ignored clients”Same shape as ignored hosts, but the key is a client label — process name (loopback) or IP (remote phone).
GET /api/v1/ignored-clientsPUT /api/v1/ignored-clients/{label}DELETE /api/v1/ignored-clients/{label}
The label segment is URL-encoded; the server decodes it.
Intercept hosts
Section titled “Intercept hosts”Hosts that pause matching requests for inspection (a coarser form of breakpoint).
GET /api/v1/intercept-hostsPUT /api/v1/intercept-hosts/{host}DELETE /api/v1/intercept-hosts/{host}
Throttle
Section titled “Throttle”GET /api/v1/throttle
Section titled “GET /api/v1/throttle”Current global throttle state.
PUT /api/v1/throttle
Section titled “PUT /api/v1/throttle”Set the global bandwidth cap.
{ "bytes_per_second": 102400 }DELETE /api/v1/throttle
Section titled “DELETE /api/v1/throttle”Disable the global throttle.
GET /api/v1/throttle-rules
Section titled “GET /api/v1/throttle-rules”List the scoped throttle rules currently loaded by the proxy. Read-only at this layer — Probe’s UI owns persistence and pushes the list on every mutation.
Sessions
Section titled “Sessions”GET /api/v1/session-tabs
Section titled “GET /api/v1/session-tabs”Active session tabs in the running app.
GET /api/v1/session-tabs/{id}
Section titled “GET /api/v1/session-tabs/{id}”Entries inside a specific tab.
GET /api/v1/session-tabs/saved
Section titled “GET /api/v1/session-tabs/saved”Sessions persisted to disk under ~/.probe/sessions/.
GET /api/v1/session-tabs/saved/{id}
Section titled “GET /api/v1/session-tabs/saved/{id}”Entries inside a saved session.
Compose
Section titled “Compose”GET /api/v1/compose/collections
Section titled “GET /api/v1/compose/collections”Array of {id, name, request_count, folder_count, environment_count, created_at}.
GET /api/v1/compose/collections/{id}
Section titled “GET /api/v1/compose/collections/{id}”Full collection JSON — folders, requests, environments, default headers, default auth.
POST /api/v1/compose/collections
Section titled “POST /api/v1/compose/collections”Create a collection.
{ "name": "Acme API", "requests": [ { "name": "List users", "method": "GET", "url": "{{base}}/users" } ], "folders": [], "environments": [ { "name": "Dev", "variables": [{"key": "base", "defaultValue": "https://dev.acme.test"}] } ]}Returns the persisted collection (with server-assigned id).
POST /api/v1/compose/collections/{id}/requests
Section titled “POST /api/v1/compose/collections/{id}/requests”Add a request to a collection. Optional folder_id to nest it.
{ "folder_id": null, "request": { "name": "Create user", "method": "POST", "url": "{{base}}/users", "headers": [ { "key": "Content-Type", "value": "application/json", "enabled": true } ], "queryParams": [], "body": "{\"name\":\"Ada\",\"email\":\"ada@example.com\"}", "contentType": "application/json", "auth": null, "timeout": 30000 }}auth is null to inherit from the collection, or a typed object: {"type": "bearer", "token": "..."}, {"type": "basic", "username": "...", "password": "..."}, {"type": "apiKey", "key": "...", "value": "..."}. Returns the persisted request with its server-assigned id.
POST /api/v1/compose/collections/{id}/environments
Section titled “POST /api/v1/compose/collections/{id}/environments”Add an environment.
{ "name": "Dev", "variables": [ { "key": "base", "defaultValue": "https://dev.acme.test", "currentValue": null, "enabled": true, "isSecret": false }, { "key": "token", "defaultValue": "", "currentValue": "abc123", "enabled": true, "isSecret": true } ]}| Field | Type | Notes |
|---|---|---|
key | string | Variable name; referenced as {{key}} in URL/headers/body. |
defaultValue | string | Persisted value. |
currentValue | string? | Session-only override. Cleared on app restart. |
enabled | bool | When false, the variable behaves as if undefined. |
isSecret | bool | Masks the value in console output and the variable hover preview. |
POST /api/v1/compose/send
Section titled “POST /api/v1/compose/send”Resolve variables and send a saved request through Probe.
{ "collection_id": "abc", "request_id": "xyz", "environment": "Dev", "variables": { "userId": "42" }}Returns the response (status, headers, body, duration_ms).
Map Local
Section titled “Map Local”GET /api/v1/map-local
Section titled “GET /api/v1/map-local”State snapshot — the master enable flag and every rule.
GET /api/v1/map-local/match?method=GET&url=https://...
Section titled “GET /api/v1/map-local/match?method=GET&url=https://...”Preview which rule (if any) would intercept the given URL.
{ "matched": true, "rule": { "id": "...", "url_pattern": "..." } }PUT /api/v1/map-local/enabled
Section titled “PUT /api/v1/map-local/enabled”{ "enabled": true }POST /api/v1/map-local/rules
Section titled “POST /api/v1/map-local/rules”Create a rule.
{ "id": "rule-1", "url_pattern": "api.example.com/users/*", "method": "GET", "match_all_methods": false, "enabled": true, "source": "inline_body", "inline_body": "{\"id\":1,\"name\":\"Ada\"}", "inline_format": "application/json", "status_code": 200, "response_headers": { "x-mocked-by": "probe" }, "delay_ms": 0}| Field | Type | Notes |
|---|---|---|
id | string | Caller-supplied; pick any stable value. |
url_pattern | string | Glob (*, **). Default :443/:80 is stripped before matching. |
method | string | Method to match. Ignored when match_all_methods is true. |
match_all_methods | bool | When true, method is ignored. Default false. |
enabled | bool | Per-rule toggle. Default true. |
source | string | "inline_body" or "local_file". Anything else falls back to inline. |
file_path | string? | Required when source = "local_file". Absolute path on the host running Probe. |
inline_body | string? | Used when source = "inline_body". Raw bytes (UTF-8 or base64-encoded for binary). |
inline_format | string? | MIME hint (application/json, text/html, image/png, …). Used to set Content-Type on the served response. |
status_code | int | HTTP status to return. Default 200. |
response_headers | object | Extra response headers. |
delay_ms | int | Artificial delay before responding. Default 0. |
Returns the persisted rule.
PUT /api/v1/map-local/rules/{id}
Section titled “PUT /api/v1/map-local/rules/{id}”Replace a rule. Body shape identical to POST.
DELETE /api/v1/map-local/rules/{id}
Section titled “DELETE /api/v1/map-local/rules/{id}”Delete by id. 204.
POST /api/v1/map-local/rules/{id}/toggle
Section titled “POST /api/v1/map-local/rules/{id}/toggle”Flip the rule’s enabled flag. Returns the updated rule.
Map Remote
Section titled “Map Remote”Same endpoint shape as Map Local under /api/v1/map-remote. The rule body differs — it splits source-match criteria from destination-rewrite fields.
GET /api/v1/map-remote
Section titled “GET /api/v1/map-remote”State snapshot — the master enable flag and every rule.
GET /api/v1/map-remote/match?method=GET&url=https://...
Section titled “GET /api/v1/map-remote/match?method=GET&url=https://...”Preview which rule (if any) would rewrite the URL. Returns the matched rule and the rewritten destination URL.
PUT /api/v1/map-remote/enabled
Section titled “PUT /api/v1/map-remote/enabled”{ "enabled": true }POST /api/v1/map-remote/rules
Section titled “POST /api/v1/map-remote/rules”Create a rule.
{ "id": "rule-1", "enabled": true, "source_scheme": "https", "source_host": "api.production.com", "source_port": null, "source_path": "/v1/*", "method": "GET", "match_all_methods": true, "dest_scheme": "https", "dest_host": "staging.example.com", "dest_port": null, "dest_path": "/v1/$1", "preserve_original_host_header": false}| Field | Type | Notes |
|---|---|---|
id | string | Caller-supplied. |
enabled | bool | Default true. |
source_scheme | string | http or https. |
source_host | string | Host to match (no port). |
source_port | int? | null means scheme default (443/80). |
source_path | string | Path glob. * captures one segment substring (regex .*, anchored). Captures are addressable in dest_path as $1, $2, … in order. |
method | string | Method to match. Ignored when match_all_methods is true. |
match_all_methods | bool | Default false. |
dest_scheme | string | http or https. |
dest_host | string | Rewrite target host. |
dest_port | int? | null means scheme default. |
dest_path | string | Rewritten path. Use $1, $2, … to substitute captures from source_path. |
preserve_original_host_header | bool | When true, the rewritten request keeps the original Host header — useful for API gateways that route by Host. Default false. |
Returns the persisted rule.
PUT /api/v1/map-remote/rules/{id}
Section titled “PUT /api/v1/map-remote/rules/{id}”Replace a rule. Body shape identical to POST.
DELETE /api/v1/map-remote/rules/{id}
Section titled “DELETE /api/v1/map-remote/rules/{id}”Delete by id. 204.
POST /api/v1/map-remote/rules/{id}/toggle
Section titled “POST /api/v1/map-remote/rules/{id}/toggle”Flip enabled. Returns the updated rule.
Scripting
Section titled “Scripting”POST /api/v1/scripts/execute
Section titled “POST /api/v1/scripts/execute”Run an ad-hoc script in test mode. Body wraps the actual input under an input key:
{ "input": { "script": "pro.test('shape', () => pro.expect(pro.response.json().id).toBeDefined());", "variables": {}, "request": { "method": "GET", "url": "https://...", "headers": {}, "body": "" }, "response": { "status": 200, "headers": {}, "body": "{\"id\":1}" }, "phase": "test" }}Returns the engine output — console messages, assertion results, mutated request/response, errors.
GET /api/v1/script-rules
Section titled “GET /api/v1/script-rules”{ "scripting_enabled": true, "rules": [ { "id": "rule-1", "name": "Inject trace header", "method": "GET", "url_pattern": "api.example.com/**", "request_script": "pro.request.headers.upsert('x-trace-id', pro.crypto.uuid());", "response_script": "", "apply_to_request": true, "apply_to_response": false, "enabled": true } ]}POST /api/v1/script-rules
Section titled “POST /api/v1/script-rules”Add a rule. The full ScriptRuleEntry shape is required:
{ "id": "rule-1", "name": "Inject trace header", "method": "GET", "url_pattern": "api.example.com/**", "request_script": "pro.request.headers.upsert('x-trace-id', pro.crypto.uuid());", "response_script": "", "apply_to_request": true, "apply_to_response": false, "enabled": true}| Field | Type | Notes |
|---|---|---|
id | string | Caller-supplied unique id. The MCP layer uses mcp_<ms> for auto-generation; pick anything stable. |
name | string | Display name in the UI. |
method | string | HTTP method (GET, POST, …) or empty if apply_to_request/apply_to_response are method-agnostic. |
url_pattern | string | Glob (*, **). Match is run against the MITM-shaped URL (with :443/:80); the matcher strips defaults so either form works. |
request_script | string | JS source for the request phase. Empty string = no request hook. |
response_script | string | JS source for the response phase. |
apply_to_request | bool | Run request_script for matching requests. |
apply_to_response | bool | Run response_script for matching responses. |
enabled | bool | Per-rule toggle. |
Returns the persisted rule. 400 if the body fails to deserialize.
PUT /api/v1/script-rules/toggle
Section titled “PUT /api/v1/script-rules/toggle”Master switch (separate from per-rule enabled).
{ "enabled": true }PUT /api/v1/script-rules/sync
Section titled “PUT /api/v1/script-rules/sync”Bulk replace the entire rule list — used by Probe’s UI to push state. Body is an array, same element shape as POST /api/v1/script-rules:
[ { "id": "rule-1", "name": "...", "method": "GET", "url_pattern": "...", "request_script": "...", "response_script": "", "apply_to_request": true, "apply_to_response": false, "enabled": true }, { "id": "rule-2", "name": "...", "method": "", "url_pattern": "**", "request_script": "", "response_script": "pro.test('200', () => pro.expect(pro.response.code).equals(200));", "apply_to_request": false, "apply_to_response": true, "enabled": true }]PUT /api/v1/script-rules/{id}
Section titled “PUT /api/v1/script-rules/{id}”Replace one rule. Body is the same ScriptRuleEntry shape as POST. The path id and the body id should match.
DELETE /api/v1/script-rules/{id}
Section titled “DELETE /api/v1/script-rules/{id}”Delete one rule. 204 on success, 404 if id is unknown.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
| 200 / 204 | Success. |
| 400 | Bad request body. |
| 401 | Missing or wrong bearer token. |
| 404 | Resource (entry id, rule id, …) doesn’t exist. |
| 500 | Server-side error — usually a parse failure on the disk-backed compose collections. |
See also
Section titled “See also”- MCP Server — high-level overview of how
probe-mcpuses this API. - MCP Tool Reference — every tool maps directly to one of the endpoints here.
- File Format — the on-disk shape of saved sessions you can fetch via
/session-tabs/saved/{id}.