HTTP API

The daemon serves this API, so both relay-core start and a foreground relay-core run expose it. Default base URL: http://127.0.0.1:8082/api/v1/; when that port is taken the daemon binds an ephemeral one and publishes the real port in $RELAY_DATA_DIR/daemon.json (relay-core status prints it).

The daemon generates a bearer token and requires Authorization: Bearer <T>; the token lives in the same 0600 manifest. The Web UI is the exception: a browser cannot attach headers to the EventSource that carries the flow stream, so the URL the daemon prints has the token in the fragment — which a browser never sends to a server — and the page trades it for a SameSite=Strict same-origin cookie, clearing the address bar. The middleware accepts the bearer header or that cookie. CORS is emitted only when --api-cors is set; otherwise the server is strict.

Proxy lifecycle

These endpoints are what "start the proxy" and "stop the proxy" look like on the wire: the CLI (start / stop) and the MCP tools (proxy_start / proxy_stop) all go through them.

GET /api/v1/proxy/status

{"phase": "running", "port": 8080, "started_at_ms": 1789817862851, "last_error": null}

POST /api/v1/proxy/start

Start the proxy. Idempotent: an already-running proxy answers 200 with already_running, a start this call performed answers 201, and a failure such as a taken port answers 409 with a stable code (start_failed) instead of success for a proxy that never bound.

{"port": 8080, "transparent": false}

POST /api/v1/proxy/stop

Stop the proxy; the daemon and its history stay. Idempotent: already_stopped when nothing was running.

POST /api/v1/daemon/shutdown

Stop the proxy and exit the daemon. Once accepted, the daemon clears its registry, stops the proxy and releases the data-directory lock.

An X-Relay-Client header may carry a client's own label (for example cli:relay-core start) which is recorded in the audit trail. It is never used for authorization; it exists so "who made this change" is answerable after the caller exits. Read those records with GET /api/v1/audit?kind=proxy_lifecycle_changed.

Version & Status

GET /api/v1/version

Engine version, API version, and a list of registered capability names.

{
  "engine_version": "0.14.0",
  "api_version": "1",
  "capabilities": ["flows", "rules", "intercepts", "mock", "metrics", "status", "events", "scripts"]
}

GET /api/v1/status

Runtime lifecycle snapshot — uptime, current rules count, intercepts pending, etc.

Flows

GET /api/v1/flows

Search and paginate captured flows. All parameters are optional.

GET /api/v1/flows?method=GET&host=api.example.com&limit=10&offset=0

Query Parameters:

  • host, path, path_contains, method — substring matches
  • status_min, status_max — status code range
  • has_error — only flows with errors
  • is_websocket — only WebSocket flows
  • limit — page size, default 50, clamped to 1–200
  • offset — pagination offset
{
  "items": [{ "id": "fld_...", "method": "GET", "path": "/users", "status": 200 }],
  "returned": 10,
  "limit": 50,
  "offset": 0
}

Redaction is applied to URLs (sensitive query keys) and headers before the response leaves the server, so secrets never appear in the result even if they were observed in traffic.

GET /api/v1/flows/:id

Full Flow JSON including headers and bodies. The same redaction rules apply.

POST /api/v1/flows/:id/replay

Re-send a captured HTTP request as stored. This call cannot edit headers or body. The replay enters through the running proxy, so it is captured as a new flow and the current rules and scripts apply. The call returns 503 when the proxy is not running. ?accept_invalid_certs=true skips TLS verification on the replay client (debug only); the engine CA is trusted automatically. WebSocket flows return 400.

GET /api/v1/flows/:id/har

Export a single flow as a HAR 1.2 entry.

GET /api/v1/flows/export/har

Batch HAR export. Accepts the same query parameters as GET /api/v1/flows.

Rules

GET /api/v1/rules

List all interception rules. Each entry is a full Rule (id, name, active, stage, priority, termination, filter, actions). The enable flag is active, not enabled.

GET /api/v1/rules

PUT /api/v1/rules

Create or replace a rule by id.

PUT /api/v1/rules
{
  "id": "rul_01HX5K9P3Q8Y2Z7",
  "name": "Tag API requests",
  "active": true,
  "stage": "RequestHeaders",
  "priority": 100,
  "termination": "Continue",
  "filter": {
    "type": "And",
    "config": [
      { "type": "Method", "config": { "mode": "Exact", "value": "GET" } },
      { "type": "Host", "config": { "mode": "Glob", "value": "*.example.com" } },
      { "type": "Path", "config": { "mode": "Glob", "value": "/api/*" } }
    ]
  },
  "actions": [{ "type": "Tag", "config": { "key": "kind", "value": "api" } }]
}

DELETE /api/v1/rules/:id

Delete a rule.

DELETE /api/v1/rules/rul_01HX5K9P3Q8Y2Z7

POST /api/v1/mock

Quickly create a MockResponse rule. Body fields: url_pattern, status, body, content_type.

Intercepts

POST /api/v1/intercepts

Create a one-shot intercept breakpoint.

{
  "url_pattern": "api.example.com",
  "phase": "request"
}

The response includes the generated rule_id used to refer to the intercept.

GET /api/v1/intercepts

List pending intercepts (both HTTP and WebSocket).

POST /api/v1/intercepts/:key/resume

Resume an intercepted flow. The body accepts action (continue or drop) and FlowModification fields: method, url, request_headers, request_header_upserts, request_body, status_code, response_headers, response_header_upserts, response_body, message_content. *_header_upserts add or overwrite by name and keep the rest; request_headers / response_headers replace the whole map.

Policy & Redaction

GET /api/v1/policy

Return the current ProxyPolicy, including the redaction block and (if configured) the upstream block. The password field of an upstream auth is masked as ***.

PATCH /api/v1/policy

Apply a partial update. The only top-level keys are redaction, upstream, and retention; unknown fields return 400. All three are optional:

{
  "redaction": {
    "enabled": true,
    "redact_bodies": true,
    "sensitive_header_names": ["authorization", "x-api-key", "cookie"],
    "sensitive_query_keys": ["token", "api_key", "password"]
  },
  "upstream": {
    "proxy_url": "http://corp-proxy.internal:8080",
    "bypass_hosts": ["*.internal", "127.0.0.1"],
    "fail_open": false
  }
}

Redaction is enabled by default. A default policy masks the sensitive header and query names below, so a fresh install does not persist or serve credentials verbatim — including to anything reading the API. Bodies are the exception: redact_bodies stays off, because masking payload content would change what rules and scripts see. Turn redaction off explicitly if you need raw headers.

Default sensitive header names: authorization, proxy-authorization, cookie, set-cookie, x-api-key, x-auth-token.

Default sensitive query keys: token, access_token, refresh_token, api_key, apikey, password, secret.

Redaction is applied to the HTTP request/response, the WebSocket handshake, every WebSocket message, flow summaries, search results, and the SSE event stream. A new proxy_url, auth, fail_open, or bypass_hosts applies to later requests, and a host matching bypass_hosts is dialled directly. If the new proxy URL cannot be resolved and fail_open is false, the previous connector stays in use.

Scripting

POST /api/v1/script

Hot-load a script into the running engine.

{ "script": "globalThis.onRequestHeaders = (ctx, flow) => { console.log(flow.layer.data.request.url); return flow; }" }

Events

GET /api/v1/events

Server-Sent Events stream of live updates.

GET /api/v1/events

Event types:

  • flow — new or updated flow (full Flow JSON in the data field)
  • ws-message — new WebSocket message
  • http-body — a body kept within budget; data is { flow_id, direction, body }
  • body-budget-exceeded — one direction exceeded the body budget; data is { flow_id, direction }
  • response-trailers — HTTP/2 trailers; data is { flow_id, trailers }
  • flow-event — typed events (intercept pause/resume, rule mutations, terminal close) alongside flow snapshots
  • audit — control-plane mutation (rule change, intercept resolved, script reloaded, policy updated)
  • audit-lagged — audit channel dropped events, server resyncs
  • lagged — flow / events channel dropped events, TUI resyncs
  • lifecycle — proxy start / stop / reload

Metrics

GET /api/v1/metrics

JSON metrics snapshot. Same fields as relay-core metrics (see CLI reference).

GET /api/v1/metrics/prometheus

Prometheus text format metrics.

# HELP relay_core_flows_total Total number of flows captured
# TYPE relay_core_flows_total counter
relay_core_flows_total 12847

# HELP relay_core_request_duration_ms Request duration in milliseconds
# TYPE relay_core_request_duration_ms histogram
relay_core_request_duration_ms_bucket{le="1"} 10023
relay_core_request_duration_ms_bucket{le="5"} 12500
relay_core_request_duration_ms_bucket{le="10"} 12800
relay_core_request_duration_ms_bucket{le="+Inf"} 12847

Script-engine metrics are emitted per hook: relay_core_script_hook_duration_us{hook}, relay_core_script_hook_invocations_total{hook}, relay_core_script_hook_errors_total{hook}, plus relay_core_script_env_access_total, relay_core_script_fetch_total, and relay_core_script_fetch_rejected_total.

Audit

GET /api/v1/audit

Query the control-plane mutation audit log.

GET /api/v1/audit?since_ms=1705276800000&actor=http&kind=policy_updated

Query Parameters:

  • since_ms, until_ms — epoch milliseconds; either may be omitted
  • actor — runtime, http, tauri, probe, or cli
  • kind — rule_changed, intercept_resolved, script_reloaded, policy_updated
  • outcome — success or failure

The response is a JSON array of AuditEvent entries. Each event records who made the change, when, and the relevant before/after diff for mutations.