Skip to content

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.

GET /api/v1/flows

Returns 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.

ParameterTypeDescription
session_idstringFilter by session ID
qstringGQL query language filter (e.g., host:api.example.com status:>399)
searchstringFull-text search across URL, host, path, notes, and tags (uses FTS5)
hoststringComma-separated list of hostnames to filter by
content_typestringContent type category filter (e.g., api, html, image)
source_appstringApplication name filter (e.g., Safari, curl)
client_ipstringClient IP address filter
device_idstringDevice ID filter (simulator UDID or IP)
limitintMaximum number of flows to return
offsetintPagination offset (skip this many flows)
sortstringColumn name to sort by
orderstringSort 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 /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 map
  • request.body — request body content
  • request.body_encoding"utf8" or "base64" indicating how the body is encoded
  • response.headers — full response header map
  • response.body — response body content
  • response.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 /api/v1/flows

Accepts 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 ID
  • session_id — delete all flows in a session
  • before — 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.

POST /api/v1/flows/{id}/replay

Re-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.

POST /api/v1/flows/replay/batch

Replays multiple flows sequentially (not in parallel) with an optional delay between each request.

Request body:

{
"flow_ids": ["id1", "id2", "id3"],
"delay_ms": 500
}
FieldRequiredConstraints
flow_idsYesMaximum 100 flows per batch
delay_msNo0–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
}
PATCH /api/v1/flows/{id}/tags

Replaces 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.

PATCH /api/v1/flows/{id}/notes

Replaces 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.

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.

GET /api/v1/flows/{id}/body/{part}/decode

Attempts 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.

GET /api/v1/flows/{id}/ws-frames

Returns 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.

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).

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"
}
]
GET /api/v1/flows/apps?session_id=...

Same format as host stats but grouped by source_app (the application that made the request).

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"
}
]
POST /api/v1/compose

Sends 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
}
POST /api/v1/compose/import/curl

Parses a cURL command string and returns the decomposed request components (method, URL, headers, body) for use in the request composer.

EventTriggerPayload
flow.createdNew flow captured by proxy, replay, or importFlow summary DTO
flow.updatedFlow tags or notes modifiedFlow summary DTO with updated fields
flow.deletedFlows removed (single, bulk, or session clear){"ids": [...]} or {"session_id": "..."}