Flows
Flows are Ghost’s primary data model — each flow represents a single HTTP request-response pair captured by the proxy. The flows API provides endpoints for listing, filtering, replaying, tagging, and inspecting captured traffic.
List Flows
Section titled “List Flows”GET /api/v1/flowsReturns lightweight flow summary objects (no request/response bodies or full headers — just method, status, host, path, timing, and size). This keeps list responses fast even with thousands of flows.
Query Parameters
Section titled “Query Parameters”| Parameter | Type | Description |
|---|---|---|
session_id | string | Filter by session ID |
q | string | GQL query language filter (e.g., host:api.example.com status:>399) |
search | string | Full-text search across URL, host, path, notes, and tags (uses FTS5) |
host | string | Comma-separated list of hostnames to filter by |
content_type | string | Content type category filter (e.g., api, html, image) |
source_app | string | Application name filter (e.g., Safari, curl) |
client_ip | string | Client IP address filter |
device_id | string | Device ID filter (simulator UDID or IP) |
limit | int | Maximum number of flows to return |
offset | int | Pagination offset (skip this many flows) |
sort | string | Column name to sort by |
order | string | Sort direction: asc or desc |
Response:
{ "flows": [ { "id": "01HWXYZ...", "session_id": "01HWXYZ...", "started_at": "2024-01-15T10:30:00Z", "duration": 150000000, "request": { "method": "GET", "url": "https://api.example.com/users", "host": "api.example.com", "path": "/users", "proto": "HTTP/2.0", "content_type": "", "content_length": 0 }, "response": { "status_code": 200, "status_text": "OK", "proto": "HTTP/2.0", "content_type": "application/json", "content_length": 4523 }, "tags": ["api"], "notes": "", "source": "proxy", "client_ip": "127.0.0.1", "source_app": "Safari", "error": "" } ], "total": 1234}Summary responses intentionally omit headers, bodies, and metadata to keep payloads small. Use the detail endpoint to get the full flow.
Get Flow Detail
Section titled “Get Flow Detail”GET /api/v1/flows/{id}Returns the complete flow including request/response headers, bodies (text as UTF-8, binary as base64), timing breakdown, metadata, and WebSocket frame count (if applicable).
Additional fields compared to the summary:
request.headers— full request header maprequest.body— request body contentrequest.body_encoding—"utf8"or"base64"indicating how the body is encodedresponse.headers— full response header mapresponse.body— response body contentresponse.body_encoding—"utf8"or"base64"metadata— key-value metadata map (noise info, replay references, addon annotations)ws_frame_count— number of WebSocket frames (if this flow is a WebSocket connection)
Body encoding logic: If the content type is a known text type (text/*, application/json, application/xml, etc.) AND the body bytes are valid UTF-8, the body is returned as a plain UTF-8 string with body_encoding: "utf8". Otherwise, it’s returned as a base64-encoded string with body_encoding: "base64". This ensures binary content (images, protobuf, compressed data) is safely transported in JSON.
Delete Flows
Section titled “Delete Flows”DELETE /api/v1/flowsAccepts a JSON request body (not query parameters):
{ "flow_ids": ["id1", "id2", "id3"], "session_id": "session_id_here", "before": "2024-01-15T10:00:00Z"}All three fields are optional, but at least one must be provided:
flow_ids— delete specific flows by IDsession_id— delete all flows in a sessionbefore— delete all flows created before this timestamp (RFC 3339 format)
Fields can be combined (e.g., delete all flows in a session before a certain time).
Response: {"deleted": 42} — the number of flows actually removed.
Broadcasts a flow.deleted WebSocket event after deletion.
Replay Flow
Section titled “Replay Flow”POST /api/v1/flows/{id}/replayRe-sends the original request through Ghost’s proxy pipeline and stores the response as a new flow. No request body needed — the handler reads the original flow from the database and reconstructs the request.
The new flow is created with:
source: "replay"(instead of"proxy")- Tags:
["replay", "replay-of:<original_id>"] - Metadata:
{"replay_of": "<original_id>"}
Response: 201 Created with the full detail DTO (including headers and body) of the newly created replay flow.
The replay request has a 30-second timeout and the response body is capped at 10 MB.
Batch Replay
Section titled “Batch Replay”POST /api/v1/flows/replay/batchReplays multiple flows sequentially (not in parallel) with an optional delay between each request.
Request body:
{ "flow_ids": ["id1", "id2", "id3"], "delay_ms": 500}| Field | Required | Constraints |
|---|---|---|
flow_ids | Yes | Maximum 100 flows per batch |
delay_ms | No | 0–10,000 ms (default: 0). Delay between consecutive replays. |
Requests execute sequentially — each replay completes before the next begins. Individual failures (flow not found, bad URL scheme, store error) are captured in the results without aborting the remaining replays.
Batch replay flows get tags: ["replay", "batch-replay", "replay-of:<id>"].
Response:
{ "results": [ { "original_id": "01HWXYZ...", "replay_id": "01HWABC...", "status_code": 200, "duration_ms": 123.45, "error": "" } ], "total": 3}Update Tags
Section titled “Update Tags”PATCH /api/v1/flows/{id}/tagsReplaces all tags on a flow (not append — sends the complete tag list):
{ "tags": ["api", "error", "important"]}To remove all tags, send an empty array: {"tags": []}. If tags is null/omitted, it’s normalized to an empty array.
Response: {"ok": true}
Broadcasts a flow.updated WebSocket event.
Update Notes
Section titled “Update Notes”PATCH /api/v1/flows/{id}/notesReplaces the notes text for a flow:
{ "notes": "This request seems to be failing intermittently during peak hours"}To clear notes, send an empty string: {"notes": ""}.
Response: {"ok": true}
Broadcasts a flow.updated WebSocket event.
Stream Body
Section titled “Stream Body”GET /api/v1/flows/{id}/body/{part}Where {part} is request or response.
Returns the raw body bytes directly (not JSON-wrapped). The Content-Type header is set from the flow’s stored content type; if empty, falls back to application/octet-stream. Content-Length is set to the exact body size.
Returns 204 No Content if the body is empty.
This endpoint streams the body as raw bytes — no base64 encoding, no JSON wrapping. Use this for downloading large response bodies or binary content directly.
Decode Body
Section titled “Decode Body”GET /api/v1/flows/{id}/body/{part}/decodeAttempts to decode protobuf/gRPC binary bodies into a human-readable format.
Response:
{ "fields": [ {"number": 1, "type": "string", "value": "hello"} ], "format": "protobuf", "readable": "1: \"hello\"\n"}The format field indicates what was detected:
"protobuf"— raw Protocol Buffer binary"grpc"— gRPC-framed Protocol Buffer"grpc-compressed"— compressed gRPC frame (not supported, returns error)"empty"— body was empty
If the content type contains “grpc”, the decoder tries gRPC frame extraction first, then falls back to raw protobuf decoding.
WebSocket Frames
Section titled “WebSocket Frames”GET /api/v1/flows/{id}/ws-framesReturns all WebSocket frames captured for a WebSocket connection flow.
Response:
{ "frames": [ { "id": "01HWXYZ...", "flow_id": "01HWABC...", "direction": "client", "opcode": 1, "opcode_name": "text", "payload": "{\"type\":\"ping\"}", "payload_encoding": "utf8", "length": 15, "timestamp": "2024-01-15T10:30:00.123Z", "seq": 1, "is_control": false } ], "total": 42}Text frames (opcode 1) have their payload encoded as UTF-8. Binary frames and control frames use base64 encoding. The total count comes from a separate database query and represents the exact frame count.
Statistics
Section titled “Statistics”Flow Stats
Section titled “Flow Stats”GET /api/v1/flows/stats?session_id=...Requires session_id. Returns aggregate statistics for the session:
{ "total_flows": 1234, "total_size": 567890, "method_counts": {"GET": 800, "POST": 434}, "status_counts": {"200": 1100, "404": 50}, "host_counts": {"api.example.com": 500}, "content_types": {"application/json": 900}, "error_count": 10, "avg_duration_ns": 50000000, "min_duration_ns": 1000000, "max_duration_ns": 5000000000}Duration values are in nanoseconds (divide by 1,000,000 to get milliseconds).
Host Stats
Section titled “Host Stats”GET /api/v1/flows/hosts?session_id=...Returns per-hostname statistics, used to populate the domain navigator in the left sidebar:
[ { "host": "api.example.com", "flow_count": 500, "error_count": 12, "avg_duration_ns": 150000000, "total_size": 2345678, "last_seen": "2024-01-15T10:35:00Z" }]App Stats
Section titled “App Stats”GET /api/v1/flows/apps?session_id=...Same format as host stats but grouped by source_app (the application that made the request).
Device Stats
Section titled “Device Stats”GET /api/v1/flows/devices?session_id=...Per-device statistics with device identification fields:
[ { "client_ip": "192.168.1.50", "device_type": "ios_simulator", "device_id": "ABC123-DEF456", "device_name": "iPhone 16 Pro", "flow_count": 200, "error_count": 5, "avg_duration_ns": 120000000, "total_size": 1234567, "last_seen": "2024-01-15T10:35:00Z" }]Request Composition
Section titled “Request Composition”Compose Request
Section titled “Compose Request”POST /api/v1/composeSends a manually constructed HTTP request (not a replay — built from scratch or forked from an existing flow):
{ "method": "GET", "url": "https://api.example.com/users", "headers": {"Authorization": "Bearer token123"}, "body": ""}Body size limit: 2 MB for the request JSON. Response body read capped at 1 MB.
Returns the response inline (not stored as a flow):
{ "status_code": 200, "status_text": "OK", "headers": {"Content-Type": "application/json"}, "body": "{\"users\":[...]}", "body_encoding": "utf8", "duration_ms": 123.45}Import cURL
Section titled “Import cURL”POST /api/v1/compose/import/curlParses a cURL command string and returns the decomposed request components (method, URL, headers, body) for use in the request composer.
WebSocket Events
Section titled “WebSocket Events”| Event | Trigger | Payload |
|---|---|---|
flow.created | New flow captured by proxy, replay, or import | Flow summary DTO |
flow.updated | Flow tags or notes modified | Flow summary DTO with updated fields |
flow.deleted | Flows removed (single, bulk, or session clear) | {"ids": [...]} or {"session_id": "..."} |