Skip to content

Graph API

The Graph API exposes the blast-radius graph that Puck builds as agents report findings. Every node is an entity (endpoint, process, user, credential, network destination) and every edge is a relationship (can-reach, runs-as, spawned, authenticated-with). You can extract subgraphs around a focus node, find all paths between two nodes, search nodes by name, and time-travel to see the graph as it looked at a past timestamp.

All graph endpoints require the graph:read scope.

Related concept page: Graph.


GET /v1/graph/subgraph

Extract an N-hop subgraph around a focus node. The response includes all nodes and edges reachable within hops steps from the focus node, optionally filtered by edge type and time window.

max_nodes caps the response size (default 5000, maximum 5000). If the subgraph would exceed this limit, the brain prunes lowest-confidence edges first and includes a truncated: true flag. Use ?edge_types=can-reach,runs-as (comma-separated) to focus on a specific threat vector.

Terminal window
curl "https://api.puck.security/v1/graph/subgraph?focus_node=node_01hx…&hops=3&edge_types=can-reach" \
-H "Authorization: Bearer $PUCK_API_KEY"
const url = new URL("https://api.puck.security/v1/graph/subgraph");
url.searchParams.set("focus_node", nodeId);
url.searchParams.set("hops", "3");
url.searchParams.set("edge_types", "can-reach,runs-as");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}` },
});
const { nodes, edges, truncated } = await res.json();
FieldInTypeRequiredDescription
focus_nodequerystringyes
hopsqueryintegerno
edge_typesquerystringno
sincequerystringno
max_nodesqueryintegerno

Status: 200 — Subgraph extraction.

Empty response body.

StatusMeaning
400Missing focus_node.
403API key lacks `graph:read`.

GET /v1/graph/path

Find all paths between two nodes up to max_hops steps. Useful for answering “how can node A reach node B?” in attack-path analysis. Returns up to max_results distinct paths.

Keep max_hops ≤ 6 for interactive use; up to 12 for offline batch analysis.

Terminal window
curl "https://api.puck.security/v1/graph/path?from=node_01hx…&to=node_02hx…&max_hops=5" \
-H "Authorization: Bearer $PUCK_API_KEY"
const url = new URL("https://api.puck.security/v1/graph/path");
url.searchParams.set("from", fromNodeId);
url.searchParams.set("to", toNodeId);
url.searchParams.set("max_hops", "5");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}` },
});
const { paths } = await res.json();
FieldInTypeRequiredDescription
fromquerystringyes
toquerystringyes
max_hopsqueryintegerno
max_resultsqueryintegerno

Status: 200 — Path search results.

Empty response body.

No documented error responses for this endpoint.


GET /v1/graph/search

Search graph nodes by external_id substring. Use this to find a node when you know part of a hostname, username, IP address, or process name but not the Puck node UUID. Filter by node_types (comma-separated) to restrict results.

Terminal window
# Find all endpoint nodes whose external_id contains "eng-laptop"
curl "https://api.puck.security/v1/graph/search?q=eng-laptop&node_types=endpoint&limit=20" \
-H "Authorization: Bearer $PUCK_API_KEY"
const url = new URL("https://api.puck.security/v1/graph/search");
url.searchParams.set("q", "eng-laptop");
url.searchParams.set("node_types", "endpoint");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}` },
});
const { nodes } = await res.json();
FieldInTypeRequiredDescription
qquerystringyes
node_typesquerystringno
limitqueryintegerno

Status: 200 — Matching nodes.

Empty response body.

No documented error responses for this endpoint.


GET /v1/graph/snapshot

Return the subgraph as it existed at a specific point in time. Edges and nodes that did not exist at at are excluded; edges that were later removed are included if they were present at at.

Time-travel snapshots are useful for incident retrospectives — you can see the blast radius as it existed when an alert fired, before any remediation changed the graph.

Terminal window
# What did the graph look like 48 hours ago?
curl "https://api.puck.security/v1/graph/snapshot?at=2026-05-01T12:00:00Z&focus_node=node_01hx…&hops=2" \
-H "Authorization: Bearer $PUCK_API_KEY"
const url = new URL("https://api.puck.security/v1/graph/snapshot");
url.searchParams.set("at", new Date(Date.now() - 48 * 3600 * 1000).toISOString());
url.searchParams.set("focus_node", nodeId);
url.searchParams.set("hops", "2");
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.PUCK_API_KEY}` },
});
const snapshot = await res.json();
FieldInTypeRequiredDescription
atquerystringyes
focus_nodequerystringno
hopsqueryintegerno
edge_typesquerystringno
max_nodesqueryintegerno

Status: 200 — Time-traveled subgraph.

Empty response body.

No documented error responses for this endpoint.


GET /v1/graph/live

Reserved — not yet available (M9).

This endpoint is defined in the spec and will deliver real-time graph updates over a WebSocket connection when it ships in milestone M9. Calling it today returns 501 Not Implemented.

No request parameters or body.

Status: 501 — Reserved; live updates land in M9.

Empty response body.

StatusMeaning
501Reserved; live updates land in M9.

GET /v1/investigations/{id}/graph

Reserved — ships in M6.

Returns the subgraph touched by the specified investigation plus an N-hop neighbourhood. Until M6, this endpoint returns 501 Not Implemented. Use GET /v1/graph/subgraph with a known node ID in the interim.

Terminal window
# Will return 501 until M6
curl "https://api.puck.security/v1/investigations/inv_01hx…/graph?hops=2" \
-H "Authorization: Bearer $PUCK_API_KEY"
FieldInTypeRequiredDescription
hopsqueryintegerno
include_provenancequerybooleanno

Status: 501 — Not implemented yet (M6).

Empty response body.

StatusMeaning
501Not implemented yet (M6).