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.
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();| Field | In | Type | Required | Description |
|---|---|---|---|---|
focus_node | query | string | yes | |
hops | query | integer | no | |
edge_types | query | string | no | |
since | query | string | no | |
max_nodes | query | integer | no |
Status: 200 — Subgraph extraction.
Empty response body.
| Status | Meaning |
|---|---|
400 | Missing focus_node. |
403 | API 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.
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();| Field | In | Type | Required | Description |
|---|---|---|---|---|
from | query | string | yes | |
to | query | string | yes | |
max_hops | query | integer | no | |
max_results | query | integer | no |
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.
# 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();| Field | In | Type | Required | Description |
|---|---|---|---|---|
q | query | string | yes | |
node_types | query | string | no | |
limit | query | integer | no |
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.
# 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();| Field | In | Type | Required | Description |
|---|---|---|---|---|
at | query | string | yes | |
focus_node | query | string | no | |
hops | query | integer | no | |
edge_types | query | string | no | |
max_nodes | query | integer | no |
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.
| Status | Meaning |
|---|---|
501 | Reserved; 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.
# Will return 501 until M6curl "https://api.puck.security/v1/investigations/inv_01hx…/graph?hops=2" \ -H "Authorization: Bearer $PUCK_API_KEY"| Field | In | Type | Required | Description |
|---|---|---|---|---|
hops | query | integer | no | |
include_provenance | query | boolean | no |
Status: 501 — Not implemented yet (M6).
Empty response body.
| Status | Meaning |
|---|---|
501 | Not implemented yet (M6). |