# Prophet API Reference

Canonical docs: https://docs.prophet.io/
Marketing site: https://prophetic.ai/
Console: https://console.prophet.io/
API base URL: https://app.prophet.io
OpenAPI: https://docs.prophet.io/openapi.json

This Markdown export is generated from the same endpoint metadata used by the interactive docs app.

## Navigation

- Overview: https://docs.prophet.io/#overview
- Quickstart: https://docs.prophet.io/#quickstart
- Authentication: https://docs.prophet.io/#authentication
- Collector config: https://docs.prophet.io/#collector-config
- Python SDK: https://docs.prophet.io/#sdk
- PQL: https://docs.prophet.io/#pql
- API reference: https://docs.prophet.io/#api
- Quickstart / API key: https://docs.prophet.io/#quickstart-api-key
- Quickstart / Token exchange: https://docs.prophet.io/#quickstart-token
- Quickstart / Deploy node: https://docs.prophet.io/#quickstart-node
- Quickstart / Registration: https://docs.prophet.io/#quickstart-registration

## API Families

- OAuth2: Issue JWT access tokens, register collectors, and run device authorization. (https://docs.prophet.io/#api-oauth2)
- Deployments: Create and manage child tenant deployments for MSP parent accounts. (https://docs.prophet.io/#api-deployments)
- Nodes: Provision units, inspect node health, trigger updates, and pull diagnostics. (https://docs.prophet.io/#api-nodes)
- Profiles: Reusable collector capture configuration for fleets and child deployments. (https://docs.prophet.io/#api-profiles)
- Collector: Download binaries and install or uninstall the Prophet collector. (https://docs.prophet.io/#api-collector)
- Search: Query flow records, request timeseries buckets, and run terms aggregations over flow data. (https://docs.prophet.io/#api-flows)
- Investigations: Read the finished investigations Prophet produces when breach signal appears — verdict, key findings, provenance lineage across access, execution, and network, and recommended actions. One question is left for a human: was this authorized? (https://docs.prophet.io/#api-investigations)
- Explore: External-organization communication shape: which external services a network sends traffic to, and the texture of each relationship (when, rhythm, transfer, who, how). Communication shape is what Prophet models to detect breaches — these endpoints expose the same view of your network for exploration. (https://docs.prophet.io/#api-explore)
- Events: Fetch event topics and records from Prophet plugin activity. (https://docs.prophet.io/#api-events)
- Automation: List, set, and delete automations scoped to a tenant or child tenant. (https://docs.prophet.io/#api-automation)

## OAuth2

Issue JWT access tokens, register collectors, and run device authorization.

### POST /rest/oauth2/token/1.0 — Exchange API credentials

Docs URL: https://docs.prophet.io/#api-oauth-token
Family: OAuth2
Auth: client credentials
Scope: public
Version: 1.0
Stability: stable

Returns a JWT bearer token from a client ID and client secret.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | client_id | string | yes | OAuth2 API credential client ID. |
| body | client_secret | string | yes | OAuth2 API credential secret. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | access_token | string | n/a | JWT bearer token. |
| response | expires_in | number | n/a | Lifetime in seconds. |
| response | expires_at | number | n/a | Unix timestamp when the token expires. |
| response | token_type | string | n/a | Usually Bearer. |

REST example:

```bash
curl -X POST https://app.prophet.io/rest/oauth2/token/1.0 \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"
```

Example response:

```json
{
  "access_token": "eyJhbGciOi...",
  "expires_in": 86400,
  "expires_at": 1770240000,
  "token_type": "Bearer"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | invalid_credentials | Client credentials are missing or invalid. |

### POST /rest/oauth2/register/1.0 — Register legacy node

Docs URL: https://docs.prophet.io/#api-oauth-register
Family: OAuth2
Auth: bearer token
Scope: p.token.scope.ingest
Version: 1.0
Stability: legacy

Registers a first-generation node by machine ID and hostname.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | machine_id | string | yes | Stable node machine identifier. |
| body | hostname | string | yes | Host name to store on the legacy node record. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | node_id | string | n/a | Existing or newly-created legacy node ID. |

Example response:

```json
{
  "node_id": "4f8ce2a8-7f2e-4c77-a7fa-904f0a9d88b1"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | missing_required_fields | machine_id or hostname is missing. |

### POST /rest/oauth2/device/1.0 — Start device authorization

Docs URL: https://docs.prophet.io/#api-oauth-device-start
Family: OAuth2
Auth: none
Scope: public
Version: 1.0
Stability: stable

Creates a pending device authorization request for a machine ID or node ID.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | machine_id | string | no | Machine identifier. Required unless node_id is supplied. |
| body | node_id | string | no | Node identifier alias. Used as machine_id when supplied. |
| body | hostname | string | yes | Host name shown during approval. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | device_code | string | n/a | Opaque code the device polls with. |
| response | auth_url | string | n/a | Full per-device approval URL on the Prophet console. Present or open it for an operator. |
| response | poll_interval | number | n/a | Seconds to wait between poll requests. |
| response | expires_in | number | n/a | Seconds until this device authorization expires. |

REST example:

```bash
curl -X POST https://app.prophet.io/rest/oauth2/device/1.0 \
  -H "Content-Type: application/json" \
  -d '{"machine_id":"machine-123","hostname":"edge-01"}'
```

Example response:

```json
{
  "device_code": "dev_abc123",
  "auth_url": "https://console.prophet.io/auth/device/dev_abc123",
  "poll_interval": 5,
  "expires_in": 900
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | error | machine_id/node_id or hostname is missing. Body is {"error":"machine_id and hostname are required"}. |

### GET /rest/oauth2/device/approve/1.0 — Get device approval info

Docs URL: https://docs.prophet.io/#api-oauth-device-info
Family: OAuth2
Auth: none
Scope: public
Version: 1.0
Stability: stable

Reads pending device details for an approval screen.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| query | device_code | string | yes | Device code returned by the start request. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | device_code | string | n/a | Device authorization code. |
| response | node_id | string \| null | n/a | Node ID when present. |
| response | hostname | string | n/a | Device hostname. |
| response | status | string | n/a | Pending, approved, denied, or expired status. |
| response | createdAt | string | n/a | Creation timestamp. |
| response | expiresAt | string | n/a | Expiration timestamp. |

Example response:

```json
{
  "device_code": "dev_abc123",
  "node_id": "node-123",
  "hostname": "edge-01",
  "status": "pending",
  "createdAt": "2026-02-04T16:00:00.000Z",
  "expiresAt": "2026-02-04T16:10:00.000Z"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | missing_required_fields | device_code is missing. |
| 404 | not_found | Device code not found. |

### POST /rest/oauth2/device/approve/1.0 — Approve or deny device

Docs URL: https://docs.prophet.io/#api-oauth-device-approve
Family: OAuth2
Auth: bearer token
Scope: any valid token
Version: 1.0
Stability: stable

Approves a pending device for the authenticated customer or denies the request.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | device_code | string | yes | Device code to approve or deny. |
| body | action | "deny" \| omitted | no | When action is deny the code is denied; otherwise it is approved. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | "approved" \| "denied" | n/a | Result of the action. |

Example response:

```json
{
  "status": "approved"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | missing_required_fields | device_code is missing. |

### POST /rest/oauth2/device/poll/1.0 — Poll device authorization

Docs URL: https://docs.prophet.io/#api-oauth-device-poll
Family: OAuth2
Auth: none
Scope: public
Version: 1.0
Stability: stable

Returns the current state of a device authorization request.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | device_code | string | yes | Device code returned by the start request. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | "pending" \| "approved" \| "denied" \| "expired" | n/a | Current device authorization status. Unknown device codes return status expired, not an error. |
| response | access_key | string \| omitted | n/a | Present once approved. A long-lived client credential in client_id.client_secret form — exchange it at /rest/oauth2/token/1.0 for bearer tokens. It is not itself a bearer token. |
| response | node_id | string \| omitted | n/a | Node identifier bound to the approval, when present. |

Example response:

```json
{
  "status": "approved",
  "access_key": "clientid.secretpart",
  "node_id": "node-123"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | error | device_code is missing. Body is {"error":"device_code is required"}. |

## Deployments

Create and manage child tenant deployments for MSP parent accounts.

### GET /rest/deployments/1.0 — List child deployments

Docs URL: https://docs.prophet.io/#api-deployments-list
Family: Deployments
Auth: bearer token
Scope: p.token.scope.deployment_api
Version: 1.0
Stability: stable

Lists child tenants for the authenticated parent MSP.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| query | parent_id | string | no | Parent MSP customer_id. Defaults to the authenticated customer_id. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 200. |
| response | parent.customer_id | string | n/a | The parent deployment customer_id used for the query. |
| response | parent.name | string \| null | n/a | Display name for the parent deployment. |
| response | parent.handle | string \| null | n/a | Parent URL-safe handle. |
| response | deployments[] | Deployment[] | n/a | Child deployments owned by the parent. |
| response | count | number | n/a | Number of child deployments returned. |

#### Python SDK

Method: `prophet.deployments.list(parent_id: str | None = None) -> list[Deployment]`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | parent_id | str \| None | no | Parent MSP customer_id. Defaults to prophet.customer_id from the JWT aud claim. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | list[Deployment] | n/a | Flat SDK model list. Returns an empty list when no children exist. |
| response | Deployment.customer_id | str | n/a | Child deployment customer_id. |
| response | Deployment.name | str \| None | n/a | Child deployment display name. |
| response | Deployment.handle | str \| None | n/a | Child deployment handle. |
| response | Deployment.type | str \| None | n/a | Usually "child" for this API. |
| response | Deployment.parent | str \| None | n/a | Parent MSP customer_id. |
| response | Deployment.created_at | str \| None | n/a | ISO timestamp from the controller. |

SDK example:

```python
from prophet.sdk import Prophet

prophet = Prophet(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
)

for deployment in prophet.deployments.list():
    print(deployment.name, deployment.customer_id)
```

REST example:

```bash
curl https://app.prophet.io/rest/deployments/1.0?parent_id=acme_msp \
  -H "Authorization: Bearer $PROPHET_TOKEN"
```

Example response:

```json
{
  "status": "success",
  "parent": {
    "customer_id": "acme_msp",
    "name": "Acme MSP",
    "handle": "acme_msp"
  },
  "deployments": [
    {
      "customer_id": "acme_msp-b5678c901",
      "name": "Client Alpha",
      "handle": "client_alpha",
      "type": "child",
      "parent": "acme_msp",
      "subdomain": "alpha",
      "deployment": {
        "status": "deployed"
      },
      "created_at": "2026-02-04T16:00:00.000Z"
    }
  ],
  "count": 1
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 403 | unauthorized | The bearer token customer is not the requested parent MSP. |

#### SDK notes

- The SDK calls GET /rest/deployments/1.0 and maps deployments[] into Deployment models.
- prophet.deployments.get(customer_id) is an SDK convenience helper that lists and filters locally.

### POST /rest/deployments/1.0 — Create child deployment

Docs URL: https://docs.prophet.io/#api-deployments-create
Family: Deployments
Auth: bearer token
Scope: p.token.scope.deployment_api
Version: 1.0
Stability: stable

Creates a child deployment under the authenticated parent MSP.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | name | string | yes | Display name for the child deployment. |
| body | handle | string | yes | URL-safe identifier. The controller slugifies it to lowercase with underscores. |
| body | parent_id | string | yes | Parent MSP customer_id. Must match the authenticated parent customer. |
| body | subdomain | string | no | Optional custom subdomain label for the child deployment. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 201. |
| response | deployment.customer.customer_id | string | n/a | Created child deployment customer_id. |
| response | deployment.customer.name | string \| null | n/a | Created child deployment display name. |
| response | deployment.customer.handle | string \| null | n/a | Slugified child handle. |
| response | deployment.customer.type | string | n/a | Set to "child". |
| response | deployment.customer.parent | string | n/a | Parent MSP customer_id. |
| response | deployment.customer.org_code | string \| null | n/a | Organization code from the identity provider. |
| response | deployment.org | object | n/a | Identity-provider organization summary returned by the create call. |

#### Python SDK

Method: `prophet.deployments.create(name: str, handle: str, parent_id: str | None = None) -> Deployment`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | name | str | yes | Display name. The SDK raises ValidationError when empty. |
| response | handle | str | yes | URL-safe identifier. The SDK raises ValidationError when empty. |
| response | parent_id | str \| None | no | Defaults to prophet.customer_id from the authenticated token. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Deployment | n/a | The SDK unwraps deployment.customer from the REST response. |
| response | Deployment.customer_id | str | n/a | Created child deployment customer_id. |
| response | Deployment.org_code | str \| None | n/a | Identity-provider organization code, preserved as an extra model field. |
| response | Deployment.deployment | dict[str, Any] \| None | n/a | Deployment status block, for example {"status": "deployed"}. |

SDK example:

```python
from prophet.sdk import Prophet

prophet = Prophet(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
)

child = prophet.deployments.create(
    name="Acme Corp",
    handle="acme_corp",
)

print(child.customer_id)
```

REST example:

```bash
curl -X POST https://app.prophet.io/rest/deployments/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme Corp","handle":"acme_corp","parent_id":"parent-123"}'
```

Example response:

```json
{
  "status": "success",
  "deployment": {
    "customer": {
      "customer_id": "acme_msp-d7890e123",
      "name": "Acme Corp",
      "handle": "acme_corp",
      "type": "child",
      "parent": "acme_msp",
      "subdomain": "acme",
      "org_code": "org_abc123def",
      "deployment": {
        "status": "deployed"
      },
      "created_at": "2026-02-04T16:00:00.000Z"
    },
    "org": {
      "code": "org_abc123def",
      "name": "Acme Corp",
      "handle": "acme_corp"
    }
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | missing_required_fields | name, handle, or parent_id is missing. |
| 403 | unauthorized | The bearer token customer cannot create children under this parent_id, or the caller is not provisioned as a parent. |

#### SDK notes

- parent_id defaults to prophet.customer_id, so parent MSP callers usually omit it.
- The SDK intentionally returns the flat Deployment model instead of the REST envelope.

### DELETE /rest/deployments/1.0 — Delete child deployment

Docs URL: https://docs.prophet.io/#api-deployments-delete
Family: Deployments
Auth: bearer token
Scope: p.token.scope.deployment_api
Version: 1.0
Stability: stable

Deletes a child deployment and its backing organization records.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | customer_id | string | yes | Child deployment customer_id to delete. |
| body | parent_id | string | yes | Parent MSP customer_id. Must own the child deployment. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 200. |
| response | message | string | n/a | Human-readable deletion confirmation. |
| response | deleted.customer_id | string | n/a | Deleted child deployment customer_id. |
| response | deleted.name | string \| null | n/a | Deleted child deployment display name. |
| response | deleted.handle | string \| null | n/a | Deleted child deployment handle. |

#### Python SDK

Method: `prophet.deployments.delete(customer_id: str, parent_id: str | None = None) -> None`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | customer_id | str | yes | Child deployment customer_id. The SDK raises ValidationError when empty. |
| response | parent_id | str \| None | no | Defaults to prophet.customer_id from the authenticated token. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | None | n/a | The SDK validates success and returns None. |

SDK example:

```python
from prophet.sdk import Prophet

prophet = Prophet(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
)

prophet.deployments.delete("acme_msp-d7890e123")
```

REST example:

```bash
curl -X DELETE https://app.prophet.io/rest/deployments/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customer_id":"acme_msp-d7890e123","parent_id":"acme_msp"}'
```

Example response:

```json
{
  "status": "success",
  "message": "Sub-deployment acme_msp-d7890e123 deleted successfully",
  "deleted": {
    "customer_id": "acme_msp-d7890e123",
    "name": "Acme Corp",
    "handle": "acme_corp"
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | missing_required_fields | customer_id or parent_id is missing. |
| 403 | unauthorized | The bearer token customer cannot manage the requested parent_id. |
| 404 | deployment_not_found | The child deployment was not found or does not belong to the parent. |

#### SDK notes

- The SDK sends customer_id and parent_id to DELETE /rest/deployments/1.0.
- Deletion cascades through the controller path that emits the customer deletion hook.

## Nodes

Provision units, inspect node health, trigger updates, and pull diagnostics.

### GET /rest/nodes/1.0 — List nodes

Docs URL: https://docs.prophet.io/#api-nodes-list
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Lists gRPC-managed nodes for the authenticated customer and child deployments.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| query | services | boolean | no | Include merged service configuration for each node. |
| query | hardware | boolean | no | Include each node reported hardware block. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | nodes[] | Node[] | n/a | Tenant-scoped nodes for the caller and child deployments when the caller is a parent MSP. |
| response | nodes[].connection | object | n/a | Derived control_plane and ingest connectivity booleans. |
| response | nodes[].network | object | n/a | local_ip and public_ip reported by the node. |
| response | nodes[].health | object | n/a | Last reported health status and timestamp. |
| response | nodes[].created_at / updated_at | string | n/a | Record timestamps. |
| response | nodes[].services | object \| omitted | n/a | Present only when services=true. |
| response | nodes[].hardware | object \| omitted | n/a | Present only when hardware=true. |

#### Python SDK

Method: `prophet.nodes.list(*, services: bool = False, hardware: bool = False) -> list[Node]`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | services | bool | no | When True, request merged service config from the API. |
| response | hardware | bool | no | When True, request reported hardware fields. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | list[Node] | n/a | Typed node models parsed from nodes[]. |
| response | Node.node_id | str | n/a | Controller-assigned node identifier. |
| response | Node.machine_id | str \| None | n/a | Stable machine identifier, often derived from a CPU ID. |
| response | Node.customer_id | str | n/a | Tenant or child deployment that owns the node. |
| response | Node.status | str \| None | n/a | Factory/enrollment state such as active, pending_approval, or staged. |
| response | Node.connection.control_plane | bool | n/a | True when the control-plane connection is live or recently healthy. |
| response | Node.connection.ingest | bool | n/a | True when ingest is connected and authenticated. |
| response | Node.collector_version | str \| None | n/a | Reported collector version. |
| response | Node.health.last_seen_at | str \| None | n/a | Last health report timestamp. |

SDK example:

```python
from prophet.sdk import Prophet

prophet = Prophet(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
)

for node in prophet.nodes.list(services=True):
    if node.is_enrolled:
        print(node.node_id, node.customer_id, node.collector_version)
```

REST example:

```bash
curl https://app.prophet.io/rest/nodes/1.0?services=true&hardware=true \
  -H "Authorization: Bearer $PROPHET_TOKEN"
```

Example response:

```json
{
  "nodes": [
    {
      "node_id": "node-123",
      "machine_id": "8efc91d2-7f2d-529b-a59b-8f3b4e6b5c2a",
      "customer_id": "acme_msp-d7890e123",
      "customer_name": "Acme Corp",
      "description": "SN-0042",
      "status": "active",
      "profile_id": "prof-1",
      "profile_name": "Acme fleet",
      "connection": {
        "control_plane": true,
        "ingest": true
      },
      "collector_version": "v0.3.0",
      "update_channel": "stable",
      "update_available": false,
      "latest_version": "v0.3.0",
      "health": {
        "status": "healthy",
        "last_seen_at": "2026-02-04T16:00:00.000Z"
      }
    }
  ]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

#### SDK notes

- Node.is_active is true when status == "active".
- Node.is_enrolled is true when the node is active and connection.control_plane is live.

### GET /rest/nodes/1.0/outdated — List outdated nodes

Docs URL: https://docs.prophet.io/#api-nodes-outdated
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Returns nodes whose collector version is behind the latest release for their channel.

#### API inputs

None documented.

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | count | number | n/a | Number of outdated nodes. |
| response | nodes[] | object[] | n/a | Compact outdated-node view. |
| response | nodes[].node_id | string | n/a | Node identifier. |
| response | nodes[].collector_version | string | n/a | Current node version. |
| response | nodes[].latest_version | string \| null | n/a | Latest version for the node update channel. |
| response | nodes[].control_plane | boolean | n/a | Whether node can receive an update right now. |

Example response:

```json
{
  "count": 1,
  "nodes": [
    {
      "node_id": "node-123",
      "customer_id": "acme_msp-d7890e123",
      "status": "active",
      "collector_version": "v0.2.0",
      "latest_version": "v0.3.0",
      "update_channel": "stable",
      "arch": "arm7",
      "control_plane": true,
      "health_status": "healthy"
    }
  ]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

### GET /rest/nodes/1.0/:node_id — Get node

Docs URL: https://docs.prophet.io/#api-nodes-get
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Returns one node, scoped to the authenticated tenant tree.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | node_id | string | yes | Controller-assigned node identifier. |
| query | services | boolean | no | Include merged service configuration. |
| query | hardware | boolean | no | Include reported hardware block. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | node_id | string | n/a | Controller-assigned node identifier. |
| response | machine_id | string \| null | n/a | Stable machine identifier. |
| response | customer_id | string | n/a | Owning tenant or child deployment. |
| response | customer_name / profile_name | string \| null | n/a | Display names for the owning tenant and applied profile. |
| response | status | string \| null | n/a | active, pending_approval, staged, or future status value. |
| response | network | object | n/a | local_ip and public_ip reported by the node. |
| response | connection | object | n/a | control_plane and ingest connectivity booleans. |
| response | health | object | n/a | Health status and last_seen_at. |
| response | created_at / updated_at | string | n/a | Record timestamps. |

#### Python SDK

Method: `prophet.nodes.get(node_id: str, *, services: bool = False, hardware: bool = False) -> Node | None`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | node_id | str | yes | Controller-assigned node identifier. The SDK raises ValidationError when empty. |
| response | services | bool | no | Request merged service config. |
| response | hardware | bool | no | Request hardware block. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Node \| None | n/a | Returns None when the API responds 404. |
| response | Node.node_id | str | n/a | Controller-assigned node identifier. |
| response | Node.machine_id | str \| None | n/a | Stable machine identifier, often derived from a CPU ID. |
| response | Node.customer_id | str | n/a | Tenant or child deployment that owns the node. |
| response | Node.status | str \| None | n/a | Factory/enrollment state such as active, pending_approval, or staged. |
| response | Node.connection.control_plane | bool | n/a | True when the control-plane connection is live or recently healthy. |
| response | Node.connection.ingest | bool | n/a | True when ingest is connected and authenticated. |
| response | Node.collector_version | str \| None | n/a | Reported collector version. |
| response | Node.health.last_seen_at | str \| None | n/a | Last health report timestamp. |

SDK example:

```python
node = prophet.nodes.get("node-123", services=True)

if node is None:
    print("node not found")
elif node.is_enrolled:
    print("node is enrolled and collecting")
```

REST example:

```bash
curl https://app.prophet.io/rest/nodes/1.0/node-123?services=true \
  -H "Authorization: Bearer $PROPHET_TOKEN"
```

Example response:

```json
{
  "node_id": "node-123",
  "machine_id": "8efc91d2-7f2d-529b-a59b-8f3b4e6b5c2a",
  "customer_id": "acme_msp-d7890e123",
  "status": "active",
  "connection": {
    "control_plane": true,
    "ingest": true
  },
  "collector_version": "v0.3.0",
  "health": {
    "status": "healthy",
    "last_seen_at": "2026-02-04T16:00:00.000Z"
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 404 | not_found | Node is missing or outside the caller tenant tree. Body is {"error":"Node not found","error_type":"not_found"}. |

#### SDK notes

- The controller intentionally returns 404 for missing nodes and nodes outside the caller tenant tree.
- Use prophet.nodes.find_by_machine_id(machine_id) when the node_id is not known before first boot.

### GET /rest/nodes/1.0/:node_id/diagnostics — Pull node diagnostics

Docs URL: https://docs.prophet.io/#api-nodes-diagnostics
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Fetches supervisor diagnostics and recent logs from a connected node.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | node_id | string | yes | Node identifier. |
| query | log_lines | number | no | Recent worker log lines to return. Controller clamps this value. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | node_id | string | n/a | Node identifier. |
| response | diagnostics | object \| null | n/a | Supervisor diagnostics report. |
| response | recent_logs | string[] | n/a | Recent worker log lines. |

Example response:

```json
{
  "node_id": "node-123",
  "diagnostics": {
    "worker_status": "running",
    "memory_budget_mb": 256
  },
  "recent_logs": [
    "collector worker healthy"
  ]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 404 | not_found | Node is missing or outside the caller tenant tree. |
| 409 | not_connected | Node control plane is not connected. |
| 504 | diagnostic_pull_failed | Supervisor request timed out or failed. |

### GET /rest/nodes/1.0/:node_id/pprof — Capture pprof profile

Docs URL: https://docs.prophet.io/#api-nodes-pprof
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Streams a raw gzipped pprof profile for connected nodes.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | node_id | string | yes | Node identifier. |
| query | type | "HEAP" \| "ALLOCS" \| "GOROUTINE" \| "CPU" \| "BLOCK" \| "MUTEX" | no | Profile type. Defaults to HEAP. |
| query | duration | number | no | Duration seconds for CPU/BLOCK/MUTEX captures; capped by controller. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | Content-Type | application/octet-stream | n/a | Raw gzipped pprof profile bytes. |
| response | Content-Disposition | header | n/a | Attachment filename formatted as <node_id>-<type>.pprof. |

Example response:

```json
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="node-123-heap.pprof"

<raw gzipped pprof bytes>
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | invalid_type | Profile type is not supported. |
| 404 | not_found | Node is missing or outside the caller tenant tree. |
| 409 | not_connected | Node control plane is not connected. |
| 504 | pprof_capture_failed | Profile capture timed out or failed. |

### POST /rest/nodes/1.0/:node_id/update — Update node

Docs URL: https://docs.prophet.io/#api-nodes-update
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Sends the latest collector release to a connected gRPC-managed node.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | node_id | string | yes | Node identifier. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | node_id | string | n/a | Node identifier. |
| response | update_status | "update_sent" \| "already_current" | n/a | Update command result from the node handler. |
| response | version | string | n/a | Latest release version for the node channel. |
| response | platform | string \| omitted | n/a | Release platform (os_arch) — present when update_status is update_sent. |

Example response:

```json
{
  "node_id": "node-123",
  "update_status": "update_sent",
  "version": "v0.3.0",
  "platform": "linux_arm7"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 404 | not_found | Node is missing or outside the caller tenant tree. |
| 409 | not_connected | Node control plane is not connected. |
| 502 | update_failed | Node update command failed. |

### POST /rest/nodes/provision/1.0 — Provision unit credential

Docs URL: https://docs.prophet.io/#api-nodes-provision
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Mints a per-unit collector credential for a target deployment.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | customer_id | string | yes | Target deployment customer_id. Can be the caller itself or one of its children. |
| body | machine_id | string | no | Stable machine identifier. The SDK can derive this from cpu_id. |
| body | description | string | no | Human label stored on the generated credential. |
| body | profile_id | string | no | Profile to inherit at first boot. The controller rejects unknown profile_id values. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 201. |
| response | access_key | string | n/a | One-time collector credential in client_id.client_secret form. |
| response | customer_id | string | n/a | Target deployment customer_id. |
| response | machine_id | string \| null | n/a | Machine identifier echoed from the request. |
| response | profile_id | string \| null | n/a | Profile associated with this credential. |

#### Python SDK

Method: `prophet.nodes.provision(deployment: str, cpu_id: str | None = None, *, machine_id: str | None = None, description: str | None = None, profile_id: str | None = None) -> ProvisionedUnit`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | deployment | str | yes | Target deployment customer_id. Sent to REST as customer_id. |
| response | cpu_id | str \| None | no | Hardware CPU ID. The SDK derives a deterministic machine_id from it. |
| response | machine_id | str \| None | no | Explicit machine_id. Overrides cpu_id derivation. |
| response | description | str \| None | no | Human label for the credential. |
| response | profile_id | str \| None | no | Capture profile to inherit at first boot. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | ProvisionedUnit | n/a | Immutable SDK result containing the one-time access_key and bootstrap helpers. |
| response | ProvisionedUnit.access_key | str | n/a | One-time client_id.client_secret credential. Secret-safe repr never prints the secret half. |
| response | ProvisionedUnit.machine_id | str | n/a | Derived or explicit machine_id. |
| response | ProvisionedUnit.customer_id | str | n/a | Target deployment customer_id. |
| response | ProvisionedUnit.profile_id | str \| None | n/a | Profile associated with this unit. |
| response | ProvisionedUnit.collector_yaml(...) | str | n/a | Renders prophet_collector.yaml content for the unit. |

SDK example:

```python
unit = prophet.nodes.provision(
    deployment="acme_msp-d7890e123",
    cpu_id="0x1122334455667788",
    description="SN-0042",
    profile_id="prof-1",
)

device_yaml = unit.collector_yaml(
    env="prod",
    spool_dir="/data/apps/prophet/spool",
)
```

REST example:

```bash
curl -X POST https://app.prophet.io/rest/nodes/provision/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"customer_id":"child-123","machine_id":"board-cpu-id","profile_id":"profile-uuid"}'
```

Example response:

```json
{
  "status": "success",
  "access_key": "clientid.secretpart",
  "customer_id": "acme_msp-d7890e123",
  "machine_id": "8efc91d2-7f2d-529b-a59b-8f3b4e6b5c2a",
  "profile_id": "prof-1"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | missing_required_fields | customer_id is missing. |
| 403 | unauthorized | Target deployment is not owned by the authenticated parent. |
| 404 | deployment_not_found | Target deployment does not exist. |
| 404 | profile_not_found | profile_id does not exist. |

#### SDK notes

- The SDK requires deployment and either cpu_id or machine_id before it sends the request.
- prophet.factory.build(...) composes this provisioning call with collector download and installer bundle generation.

### POST /rest/nodes/1.0/:node_id/manage — Legacy deploy-node manage

Docs URL: https://docs.prophet.io/#api-nodes-manage
Family: Nodes
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: legacy

Legacy operations for first-generation deploy nodes.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | node_id | string | yes | Legacy deploy node identifier. |
| body | operation | "status" \| "logs" \| "pull" \| "restart" \| "upgrade" | yes | Legacy socket operation to run. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | node_id | string | n/a | Legacy deploy node identifier. |
| response | operation | string | n/a | Operation that was requested. |
| response | result | object | n/a | Socket response from the legacy deploy node. |

Example response:

```json
{
  "node_id": "legacy-node-123",
  "operation": "status",
  "result": {
    "status": "ok"
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | unknown_operation | operation is not one of status, logs, pull, restart, upgrade. |
| 404 | not_connected | Legacy deploy node is not connected. |
| 504 | timeout | Legacy socket operation timed out. |

## Profiles

Reusable collector capture configuration for fleets and child deployments.

### GET /rest/profiles/1.0 — List profiles

Docs URL: https://docs.prophet.io/#api-profiles-list
Family: Profiles
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Lists reusable node capture profiles for the authenticated tenant tree.

#### API inputs

None documented.

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 200. |
| response | profiles[] | Profile[] | n/a | Profiles owned by the caller and child deployments when caller is a parent MSP. |
| response | count | number | n/a | Number of profiles returned. |

#### Python SDK

Method: `prophet.profiles.list() -> list[Profile]`

None documented.

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | list[Profile] | n/a | Typed profile models parsed from profiles[]. |
| response | Profile.profile_id | str | n/a | Reusable capture profile identifier. |
| response | Profile.customer_id | str | n/a | Tenant that owns the profile. |
| response | Profile.name | str | n/a | Display name. |
| response | Profile.description | str \| None | n/a | Optional notes. |
| response | Profile.services | dict[str, Any] \| None | n/a | Per-service capture config (packet, host_logs, netflow, suricata_logs). Omitted fields use server defaults. See the Collector config section for the full schema, defaults, and gotchas. |
| response | Profile.tags | list[str] \| None | n/a | Optional tags applied to nodes using this profile. |
| response | Profile.update_channel | "stable" \| "dev" \| "pinned" \| str \| None | n/a | Collector update channel. |
| response | Profile.fleet_staging | bool \| None | n/a | Whether provision-token nodes start staged. |

SDK example:

```python
for profile in prophet.profiles.list():
    print(profile.profile_id, profile.name)
```

REST example:

```bash
curl https://app.prophet.io/rest/profiles/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN"
```

Example response:

```json
{
  "status": "success",
  "profiles": [
    {
      "profile_id": "prof-1",
      "customer_id": "acme_msp",
      "name": "Acme fleet",
      "services": {
        "packet": {
          "enabled": true,
          "lightweight": true
        }
      },
      "update_channel": "stable",
      "fleet_staging": false
    }
  ],
  "count": 1
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

#### SDK notes

- A parent MSP can list profiles owned by itself and child deployments.
- Use profile.profile_id when provisioning units with prophet.nodes.provision(...).

### POST /rest/profiles/1.0 — Create profile

Docs URL: https://docs.prophet.io/#api-profiles-create
Family: Profiles
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Creates a reusable capture configuration profile.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | name | string | yes | Profile display name. |
| body | description | string | no | Optional notes for operators. |
| body | services | object | no | Capture-config service blocks. A submitted service block is stored as-is; omitted services and fields fall back to server defaults when the profile is delivered to a node. |
| body | tags | string[] | no | Tags applied to nodes using this profile. |
| body | update_channel | "stable" \| "dev" \| "pinned" | no | Collector update channel. SDK default is stable. |
| body | fleet_staging | boolean | no | When true, provision-token nodes start staged. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 201. |
| response | profile | Profile | n/a | Created profile document. Each submitted service block is echoed as-is; per-field defaults are applied when the profile is delivered to a node, not at create time. |

#### Python SDK

Method: `prophet.profiles.create(name: str, *, description: str | None = None, services: ProfileServices | dict[str, Any] | None = None, tags: list[str] | None = None, update_channel: Literal["stable", "dev", "pinned"] = "stable", fleet_staging: bool = False) -> Profile`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | name | str | yes | Profile display name. The SDK raises ValidationError when empty. |
| response | description | str \| None | no | Optional notes. |
| response | services | ProfileServices \| dict[str, Any] \| None | no | Typed service config or raw dict escape hatch. |
| response | tags | list[str] \| None | no | Optional node tags. |
| response | update_channel | "stable" \| "dev" \| "pinned" | no | SDK default is stable. |
| response | fleet_staging | bool | no | SDK default is False. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Profile | n/a | Typed SDK model from response.profile. |
| response | Profile.profile_id | str | n/a | Reusable capture profile identifier. |
| response | Profile.customer_id | str | n/a | Tenant that owns the profile. |
| response | Profile.name | str | n/a | Display name. |
| response | Profile.description | str \| None | n/a | Optional notes. |
| response | Profile.services | dict[str, Any] \| None | n/a | Per-service capture config (packet, host_logs, netflow, suricata_logs). Omitted fields use server defaults. See the Collector config section for the full schema, defaults, and gotchas. |
| response | Profile.tags | list[str] \| None | n/a | Optional tags applied to nodes using this profile. |
| response | Profile.update_channel | "stable" \| "dev" \| "pinned" \| str \| None | n/a | Collector update channel. |
| response | Profile.fleet_staging | bool \| None | n/a | Whether provision-token nodes start staged. |

SDK example:

```python
from prophet.sdk.profiles import lightweight_packet_services

profile = prophet.profiles.create(
    name="Acme fleet",
    services=lightweight_packet_services(interface_patterns=["eth*"]),
    tags=["edge"],
)

print(profile.profile_id)
```

REST example:

```bash
curl -X POST https://app.prophet.io/rest/profiles/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Acme fleet","services":{"packet":{"enabled":true,"lightweight":true}}}'
```

Example response:

```json
{
  "status": "success",
  "profile": {
    "profile_id": "prof-1",
    "customer_id": "acme_msp",
    "name": "Acme fleet",
    "services": {
      "packet": {
        "enabled": true,
        "lightweight": true
      }
    },
    "update_channel": "stable",
    "auto_update": false,
    "fleet_staging": false,
    "created_at": "2026-02-04T16:00:00.000Z",
    "updated_at": "2026-02-04T16:00:00.000Z"
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | missing_required_fields | name is missing. |

#### SDK notes

- ProfileServices omits unset fields; per-field defaults are applied when the profile is delivered to a node.
- extra="forbid" in the typed service models catches misspelled service fields before the request is sent.

### DELETE /rest/profiles/1.0/:profile_id — Delete profile

Docs URL: https://docs.prophet.io/#api-profiles-delete
Family: Profiles
Auth: bearer token
Scope: p.token.scope.node_api
Version: 1.0
Stability: stable

Deletes a profile owned by the authenticated tenant tree.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | profile_id | string | yes | Profile identifier to delete. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | string | n/a | Always "success" on 200. |
| response | deleted.profile_id | string | n/a | Deleted profile identifier. |

#### Python SDK

Method: `prophet.profiles.delete(profile_id: str) -> None`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | profile_id | str | yes | Profile identifier. The SDK raises ValidationError when empty. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | None | n/a | The SDK validates success and returns None. |

SDK example:

```python
prophet.profiles.delete("prof-1")
```

REST example:

```bash
curl -X DELETE https://app.prophet.io/rest/profiles/1.0/prof-1 \
  -H "Authorization: Bearer $PROPHET_TOKEN"
```

Example response:

```json
{
  "status": "success",
  "deleted": {
    "profile_id": "prof-1"
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 404 | not_found | Profile is missing or outside the caller tenant tree. |

#### SDK notes

- The API only deletes profiles owned by the authenticated tenant tree.

## Collector

Download binaries and install or uninstall the Prophet collector.

### GET /rest/collector/download/1.0 — Download collector binary

Docs URL: https://docs.prophet.io/#api-collector-download
Family: Collector
Auth: none
Scope: public
Version: 1.0
Stability: stable

Redirects to a signed GitHub release asset for the latest collector binary.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| query | os | "linux" \| "darwin" \| "windows" | no | Target operating system. Defaults to linux. Supported os/arch pairs: linux/amd64, linux/arm7, darwin/arm64, windows/amd64. |
| query | arch | "amd64" \| "arm7" \| "arm64" | no | Target architecture. Defaults to amd64. Only the four supported os/arch pairs are valid — there is no linux/arm64 build. |
| query | channel | "stable" \| "dev" | no | Release channel. Defaults to stable in production, dev in dev. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | 302 Location | URL | n/a | Temporary signed release asset URL. |
| response | Content-Disposition | header | n/a | Versioned filename when the signed asset response is followed. |
| response | body | application/octet-stream | n/a | Release tarball containing the prophet binary. |

#### Python SDK

Method: `prophet.collector.download(dest: str | Path | None = None, *, os: OS = "linux", arch: Arch = "amd64", channel: Channel = "stable", extract: bool = False, cache: bool = True) -> Path`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | dest | str \| Path \| None | no | Tarball path, or output directory when extract=True. |
| response | os | "linux" \| "darwin" \| "windows" | no | SDK default is linux. |
| response | arch | "amd64" \| "arm7" \| "arm64" | no | SDK default is amd64. |
| response | channel | "stable" \| "dev" | no | SDK default is stable. |
| response | extract | bool | no | When True, unpack the archive and return the prophet binary path. |
| response | cache | bool | no | Reuse cached versioned binaries. Set False to force a fresh fetch. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | download_url(...) | str | n/a | Controller URL that redirects to the latest signed binary. |
| response | download(...) | Path | n/a | Path to downloaded tarball, or extracted prophet binary when extract=True. |

SDK example:

```python
# URL only, useful for install scripts.
url = prophet.collector.download_url(arch="arm7")

# Download and extract the latest stable ARM7 collector.
binary = prophet.collector.download(
    arch="arm7",
    extract=True,
    dest="./dist",
)

print(binary)
```

REST example:

```bash
curl -L "https://app.prophet.io/rest/collector/download/1.0?os=linux&arch=arm7&channel=stable" \
  -o prophet_collector_linux_arm7.tar.gz
```

Example response:

```json
HTTP/1.1 302 Found
Location: https://github-releases.githubusercontent.com/signed/asset/url

# Follow the redirect to receive a versioned .tar.gz collector archive.
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | error | Unsupported os/arch pair. Body is {"error":"Unsupported platform: <os>/<arch>","supported":[...]} listing the valid pairs. |
| 404 | error | No release, no assets, or no binary for the requested channel/platform. Body is {"error":"<reason>"}. |
| 500 | error | Release lookup failed. Body is {"error":"<reason>"}. |

#### SDK notes

- The SDK caches downloads by versioned Content-Disposition filename under ~/.cache/prophet-sdk/collector (override the root with PROPHET_SDK_CACHE_DIR).
- When extract=True, the SDK unpacks prophet safely and chmods it executable.
- prophet.factory.build(...) uses this API while assembling an install bundle.

### GET /rest/collector/install/1.0 — Get install script

Docs URL: https://docs.prophet.io/#api-collector-install
Family: Collector
Auth: none
Scope: public
Version: 1.0
Stability: stable

Returns a Bash installer for Linux and macOS collectors.

#### API inputs

None documented.

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | Content-Type | text/plain; charset=utf-8 | n/a | Bash installer script. |
| response | Content-Disposition | inline | n/a | Script is intended to be piped to a shell or reviewed inline. |
| response | body | bash | n/a | Detects OS/architecture, downloads the collector, installs the binary, and configures systemd or launchd. |

REST example:

```bash
curl -sSL https://app.prophet.io/rest/collector/install/1.0 \
  | sudo bash -s -- "$PROPHET_PROVISION_TOKEN"
```

Example response:

```json
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline

#!/bin/bash
set -e
# Prophet Collector Installer
# Usage: curl -sSL https://app.prophet.io/rest/collector/install/1.0 | bash -s -- <provision_token>
```

### GET /rest/collector/uninstall/1.0 — Get uninstall script

Docs URL: https://docs.prophet.io/#api-collector-uninstall
Family: Collector
Auth: none
Scope: public
Version: 1.0
Stability: stable

Returns a Bash uninstaller for collector service and local files.

#### API inputs

None documented.

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | Content-Type | text/plain; charset=utf-8 | n/a | Bash uninstall script. |
| response | Content-Disposition | inline | n/a | Script is intended to be piped to a shell or reviewed inline. |
| response | body | bash | n/a | Stops and removes systemd or launchd service files, removes the collector binary, and removes local configuration. |

REST example:

```bash
curl -sSL https://app.prophet.io/rest/collector/uninstall/1.0 \
  | sudo bash
```

Example response:

```json
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline

#!/bin/bash
set -e
# Prophet Collector Uninstaller
```

## Search

Query flow records, request timeseries buckets, and run terms aggregations over flow data.

### POST /search/records/1.0 — Query flow records

Docs URL: https://docs.prophet.io/#api-flows-query
Family: Search
Auth: bearer token
Scope: p.token.scope.search_api
Version: 1.0
Stability: stable

Queries flow records for one or more instances with PQL, time filters, sorting, field selection, pagination, and optional timeseries buckets.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | instance_ids | string[] | yes | Instances or customer IDs to search. |
| body | module | "flows" | yes | Must be flows for flow record search. |
| body | sentence | string | no | PQL query. Empty means match all. |
| body | start | DateTimeFilter | no | Start time filter. Defaults to relative 15 minutes. |
| body | end | DateTimeFilter | no | End time filter. Defaults to now. |
| body | sort | { field: string, order: "asc" \| "desc" }[] | no | Sort entries, applied in order. Defaults to @timestamp desc. |
| body | fields | string[] | no | Fields to include. Missing dotted paths are omitted; wildcard "*" is rejected. @timestamp is always included; the injected record id is dropped unless requested. |
| body | size | number | no | Page size. Default is 15 in REST, SDK default is 100, maximum is 25000. size=0 is only valid with timeseries. |
| body | page | number | no | Zero-based page number. Offset is page * size. |
| body | timeseries.buckets | number | no | Auto date histogram bucket count. Use when interval is not supplied. |
| body | timeseries.interval | string | no | Fixed date histogram interval such as 30s, 5m, 1h, or 1d. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | <instance_id>.flows[] | object[] | n/a | Flow records. With multiple instance_ids the search runs across all of them merged, and each instance key carries the same merged result set. Each record carries an injected id (the backing document id) unless a fields projection is used. |
| response | <instance_id>.id | string | n/a | Opaque per-response identifier; not stable across requests. |
| response | <instance_id>.found | number | n/a | Matching document count. |
| response | <instance_id>.total | number | n/a | Total record count for the searched window. |
| response | <instance_id>.returned | number | n/a | Records returned in this page. |
| response | <instance_id>.page_size | number | n/a | Page size used by this response. |
| response | <instance_id>.current_page | number | n/a | Currently always 0 regardless of the requested page; track pagination from your request. Offset math itself honors the requested page. |
| response | <instance_id>.next_page | number | n/a | 1 when more_data_available, otherwise 0. |
| response | <instance_id>.pages | number | n/a | Total page count: found divided by page_size, rounded up. |
| response | <instance_id>.more_data_available | boolean | n/a | True when found exceeds page_size. Not page-aware: it stays true on the last page of a large result. |
| response | <instance_id>.count_aggregation[] | object[] \| omitted | n/a | Timeseries buckets with time and count when timeseries is requested. |
| response | <instance_id>.interval | string \| omitted | n/a | Histogram interval returned for timeseries queries. |
| response | <instance_id>.interval_period | string \| omitted | n/a | Unit suffix of the histogram interval (for example h for hours). |
| response | <instance_id>.took | number | n/a | Search execution time in seconds. |
| response | results_for[] | string[] | n/a | Instance IDs searched. |
| response | total_time_ms | number | n/a | End-to-end handler execution time. |
| response | success | boolean | n/a | True when the request completed. |

#### Python SDK

Method: `prophet.flows.query(instance: str, query: str | Q = "", start: TimeFilter | None = None, end: TimeFilter | None = None, sort: list[Sort] | None = None, fields: list[str] | None = None, size: int = 100) -> FlowIterator`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instance | str | yes | Single instance/customer ID. SDK sends instance_ids: [instance]. |
| response | query | str \| Q | no | Raw PQL string or fluent Q builder. |
| response | start | TimeFilter \| None | no | Now, MinutesAgo, HoursAgo, DaysAgo, WeeksAgo, or At. |
| response | end | TimeFilter \| None | no | End time filter. |
| response | sort | list[Sort] \| None | no | Sort objects converted to API sort payloads. |
| response | fields | list[str] \| None | no | Response fields to include. |
| response | size | int | no | Page size. SDK default is 100. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | FlowIterator | n/a | Lazy iterator that auto-fetches pages as you iterate. |
| response | FlowIterator.take(n) | FlowIterator | n/a | Limit total results across pages. |
| response | FlowIterator.first() | FlowPage | n/a | Fetch only the first page. |
| response | FlowIterator.collect(limit=None) | list[Flow] | n/a | Collect results eagerly, optionally bounded by limit. |
| response | FlowIterator.next_page() | FlowPage \| None | n/a | Fetch the next page directly; None once exhausted. |
| response | FlowIterator.total_found | int \| None | n/a | Matching document count; None before the first fetch. |
| response | FlowPage.flows | list[Flow] | n/a | Typed flow models for the current page. |
| response | FlowPage.found | int | n/a | Total matching documents. |
| response | FlowPage.total | int | n/a | Index-wide record count returned by the search service. |
| response | FlowPage.returned | int | n/a | Flow records returned in this page. |
| response | FlowPage.current_page | int | n/a | Zero-based page number. |
| response | FlowPage.page_count | int | n/a | Total page count reported by search. |
| response | FlowPage.has_more | bool | n/a | True when another page can be fetched. |
| response | FlowPage.took | float | n/a | Search execution time in seconds. |
| response | Flow.src.ip / Flow.dst.ip | str \| None | n/a | Structured source and destination endpoint addresses. |
| response | Flow.src_ip / Flow.dst_ip | str | n/a | Convenience properties for source and destination addresses. |
| response | Flow.bytes / Flow.packets | float | n/a | Convenience totals derived from metric or stats fields. |
| response | Flow.protocol | str | n/a | Transport protocol convenience value. |

SDK example:

```python
from prophet.sdk import Q, HoursAgo, Now, Sort

flows = prophet.flows.query(
    instance="acme_msp-d7890e123",
    query=Q("dst.port").eq(443),
    start=HoursAgo(24),
    end=Now(),
    sort=[Sort("@timestamp", "desc")],
    fields=[
        "src.ip",
        "dst.ip",
        "stats.volume.bytes.total",
        "app_name",
    ],
    size=100,
)

for flow in flows.take(500):
    print(flow.src.ip, flow.dst.ip, flow.bytes, flow.app_name)
```

REST example:

```bash
curl -X POST https://app.prophet.io/search/records/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_ids": ["acme_msp-d7890e123"],
    "module": "flows",
    "sentence": "dst.port eq 443",
    "start": { "relative": { "value": 24, "unit": "hours" } },
    "end": { "now": true },
    "size": 100,
    "page": 0,
    "timeseries": { "interval": "1h" }
  }'
```

Example response:

```json
{
  "acme_msp-d7890e123": {
    "flows": [
      {
        "src": { "ip": "10.0.0.10", "port": 51244 },
        "dst": { "ip": "198.51.100.10", "port": 443 },
        "transport": { "proto": "TCP" },
        "stats": {
          "volume": {
            "bytes": { "total": 1024 }
          }
        }
      }
    ],
    "found": 4183,
    "total": 88421,
    "returned": 100,
    "page_size": 100,
    "current_page": 0,
    "next_page": 1,
    "pages": 42,
    "more_data_available": true,
    "count_aggregation": [
      { "time": 1770220800, "count": 18 },
      { "time": 1770224400, "count": 24 }
    ],
    "interval": "1h",
    "interval_period": "h",
    "took": 0.15
  },
  "results_for": ["acme_msp-d7890e123"],
  "total_time_ms": 151,
  "success": true
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | validation_error | Invalid body, module, instance_ids, date filter, sort, fields, size/page, or PQL parsing failure. |
| 500 | search_error | Search execution failed. |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

#### SDK notes

- prophet.flows(...) is a shortcut for prophet.flows.query(...).
- The SDK maps each flow record into a tolerant Flow model with dot-access fields such as flow.src.ip, flow.dst.ip, flow.transport.proto, and flow.stats.volume.bytes.total.
- Shortcut properties such as flow.src_ip, flow.dst_ip, flow.protocol, flow.bytes, and flow.packets are available for common reads.
- The current Python SDK covers records search and pagination. Terms aggregation is REST-only today.
- 400 search validation errors are raised as APIError with error_type="validation_error".

### POST /search/agg/1.0 — Aggregate flow terms

Docs URL: https://docs.prophet.io/#api-flows-terms-agg
Family: Search
Auth: bearer token
Scope: p.token.scope.search_api
Version: 1.0
Stability: stable

Groups matching flows by a field and computes sum, average, or cardinality metrics for each bucket.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | module | "terms_agg" | yes | Must be terms_agg for the aggregation endpoint. |
| body | instance_id | string | yes | Single instance/customer ID to aggregate. |
| body | sentence | string | no | PQL query. Empty means match all. |
| body | start | DateTimeFilter | no | Start time filter. Defaults to relative 15 minutes. |
| body | end | DateTimeFilter | no | End time filter. Defaults to now. |
| body | size | number | no | Maximum buckets. Defaults to 100; max is 10000. |
| body | agg.field | string | yes | Field to group by. Must be an aggregatable (keyword-mapped) field; free-text fields fail. |
| body | agg.by[].field | string | yes | Metric field to aggregate. |
| body | agg.by[].metric | "sum" \| "avg" \| "cardinality" | yes | Metric aggregation type. |
| body | agg.by[].order | "asc" \| "desc" | yes | Bucket ordering for this metric. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | "success" \| "error" | n/a | Aggregation status. |
| response | results.aggregation[] | object[] | n/a | Buckets. Each bucket flattens the grouped field, count, requested metrics, and an optional sample flow. |
| response | results.aggregation[].count | number | n/a | Document count in the bucket. |
| response | results.types | Record<string, string> | n/a | Simple metric type metadata such as bytes, packets, duration, or integer. |
| response | results.flows[] | object[] | n/a | Sample flow documents, one per bucket when available. |
| response | results.found | number | n/a | Total matching documents. |
| response | results.total_distinct | number | n/a | Cardinality of the grouped field. |
| response | results.returned | number | n/a | Buckets returned. |
| response | customer_id | string | n/a | Echoed instance/customer ID. |
| response | errors | string \| omitted | n/a | Error text when status is error. |

REST example:

```bash
curl -X POST https://app.prophet.io/search/agg/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "module": "terms_agg",
    "instance_id": "acme_msp-d7890e123",
    "sentence": "dst.port eq 443",
    "size": 10,
    "start": { "relative": { "value": 24, "unit": "hours" } },
    "end": { "now": true },
    "agg": {
      "field": "dst.ip",
      "by": [
        { "field": "stats.volume.bytes.total", "metric": "sum", "order": "desc" },
        { "field": "src.ip", "metric": "cardinality", "order": "desc" }
      ]
    }
  }'
```

Example response:

```json
{
  "status": "success",
  "results": {
    "aggregation": [
      {
        "dst.ip": "198.51.100.10",
        "count": 37,
        "stats.volume.bytes.total": 52428800,
        "src.ip": 18,
        "flow": {
          "src": { "ip": "10.0.0.10" },
          "dst": { "ip": "198.51.100.10", "port": 443 }
        }
      }
    ],
    "types": {
      "stats.volume.bytes.total": "integer",
      "src.ip": "integer"
    },
    "flows": [
      {
        "src": { "ip": "10.0.0.10" },
        "dst": { "ip": "198.51.100.10", "port": 443 }
      }
    ],
    "found": 420,
    "total_distinct": 73,
    "returned": 1
  },
  "customer_id": "acme_msp-d7890e123"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 400 | error | Validation failure. Body is {"status":"error","errors":"<text>"} — text prefixed with Invalid request body, Request validation failed, Invalid date filter, or PQL parsing failed. |
| 500 | error | Aggregation failure. Body is {"status":"error","errors":"Terms aggregation failed: <text>"}. |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

## Investigations

Read the finished investigations Prophet produces when breach signal appears — verdict, key findings, provenance lineage across access, execution, and network, and recommended actions. One question is left for a human: was this authorized?

### GET /rest/investigations/1.0 — List investigations

Docs URL: https://docs.prophet.io/#api-investigations-list
Family: Investigations
Auth: bearer token
Scope: p.token.scope.investigations_api
Version: 1.0
Stability: stable

Lists Apollo investigation rollups for the authenticated tenant, filterable by verdict, confidence, and time, most severe first.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| query | disposition | "benign" \| "malicious" \| "escalate" | no | Filter by Apollo's verdict. |
| query | min_confidence | number | no | Only verdicts with confidence >= this (0..1). |
| query | since | string (ISO-8601) | no | Only investigations created at/after this time. |
| query | until | string (ISO-8601) | no | Only investigations created at/before this time. |
| query | sort | "severity" \| "recent" | no | Order. Default severity (most severe first). |
| query | limit | number | no | Page size, clamped to 1..200. Default 50. |
| query | offset | number | no | Page offset. Default 0. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | investigations[] | InvestigationListItem[] | n/a | Compact rollup rows for the current page. |
| response | investigations[].id | string | n/a | Investigation id. Pass to GET /rest/investigations/1.0/{id}. |
| response | investigations[].status | "running" \| "completed" \| "failed" | n/a | Lifecycle state. |
| response | investigations[].disposition | "benign" \| "malicious" \| "escalate" \| null | n/a | Apollo's verdict. null while running. |
| response | investigations[].confidence | number \| null | n/a | Verdict confidence 0..1. null while running. |
| response | investigations[].headline | string \| null | n/a | One-line plain-language summary of the investigation. |
| response | investigations[].source | string \| null | n/a | Originating entity of the flagged activity. |
| response | investigations[].destination | string \| null | n/a | Counterparty of the flagged activity. |
| response | investigations[].detected_at | string \| null | n/a | When the triggering activity was flagged. |
| response | investigations[].created_at | string \| null | n/a | When Apollo opened the investigation. |
| response | investigations[].completed_at | string \| null | n/a | When Apollo closed it. null while running. |
| response | investigations[].related_alerts_count | number | n/a | Near-duplicate detections auto-linked into this one investigation. |
| response | investigations[].tags | string[] | n/a | Free-form labels applied to the investigation. |
| response | total | number | n/a | Total investigations matching the filter (for pagination). |
| response | limit | number | n/a | Page size used by this response. |
| response | offset | number | n/a | Page offset used by this response. |

#### Python SDK

Method: `prophet.investigations.list(*, disposition: str | None = None, min_confidence: float | None = None, since: str | datetime | None = None, until: str | datetime | None = None, sort: str | None = None, limit: int = 50, offset: int = 0) -> InvestigationPage`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | disposition | str \| None | no | Filter by verdict: benign, malicious, or escalate. |
| response | min_confidence | float \| None | no | Only verdicts with confidence >= this (0..1). |
| response | since | str \| datetime \| None | no | Created-at lower bound. A datetime is serialized to ISO-8601. |
| response | until | str \| datetime \| None | no | Created-at upper bound. |
| response | sort | str \| None | no | "severity" (default) or "recent". |
| response | limit | int | no | Page size (default 50, max 200). |
| response | offset | int | no | Page offset (default 0). |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | InvestigationPage | n/a | Iterable page of InvestigationListItem, with .total, .has_more, and .items. |
| response | InvestigationListItem.id | str | n/a | Investigation id. |
| response | InvestigationListItem.disposition | str \| None | n/a | benign \| malicious \| escalate (None while running). |
| response | InvestigationListItem.confidence | float \| None | n/a | Verdict confidence. |
| response | InvestigationListItem.headline | str \| None | n/a | One-line summary. |
| response | InvestigationListItem.is_malicious / .needs_escalation / .is_running | bool | n/a | Convenience checks over disposition/status. |

SDK example:

```python
from prophet.sdk import Prophet

prophet = Prophet(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
)

# Newest escalations first
page = prophet.investigations.list(disposition="escalate", sort="recent", limit=25)
print(page.total)
for inv in page:
    print(inv.headline, inv.confidence)

# Stream every match across all pages (auto-pagination)
for inv in prophet.investigations.iter(disposition="malicious"):
    print(inv.id, inv.confidence)
```

REST example:

```bash
curl -G https://app.prophet.io/rest/investigations/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -d "disposition=escalate" -d "sort=recent" -d "limit=25"
```

Example response:

```json
{
  "investigations": [
    {
      "id": "inv_43d67a...",
      "status": "completed",
      "disposition": "escalate",
      "confidence": 0.85,
      "headline": "Escalate: a first-time 616 MB export to Google Drive under valid credentials; intent unconfirmed.",
      "source": "10.90.8.16",
      "destination": "google",
      "detected_at": "2026-06-29T20:00:00Z",
      "created_at": "2026-06-30T22:46:37Z",
      "completed_at": "2026-06-30T23:01:09Z",
      "related_alerts_count": 0,
      "tags": ["exfil"]
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | invalid_request | Unknown query parameter or invalid enum/range. |
| 502 | upstream_error | Upstream engine returned a failure. |
| 503 | service_unavailable | No engine is available for the tenant. |
| 504 | upstream_timeout | Upstream request timed out (30s). |

#### SDK notes

- prophet.investigations.iter(*, disposition=None, min_confidence=None, since=None, until=None, sort=None, page_size=100, start_offset=0) streams InvestigationListItem across all pages (auto-pagination; page_size clamped to 1..200).
- The SDK speaks the external vocabulary — pass disposition="escalate"; the mapping to internal values happens server-side.

### GET /rest/investigations/1.0/:id — Get investigation

Docs URL: https://docs.prophet.io/#api-investigations-get
Family: Investigations
Auth: bearer token
Scope: p.token.scope.investigations_api
Version: 1.0
Stability: stable

Returns one full Apollo investigation — verdict, key findings, provenance lineage, and recommended actions. Available once analysis completes.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | id | string | yes | Investigation id from the list endpoint. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | id | string | n/a | Investigation id. |
| response | status | "running" \| "completed" \| "failed" | n/a | Lifecycle state (completed for any record returned). |
| response | created_at | string \| null | n/a | When Apollo opened the investigation. |
| response | completed_at | string \| null | n/a | When Apollo closed it. |
| response | trigger.source | string \| null | n/a | Originating entity of the flagged activity. |
| response | trigger.destination | string \| null | n/a | Counterparty of the flagged activity. |
| response | trigger.detected_at | string \| null | n/a | When the triggering activity was flagged. |
| response | trigger.signal.volume_bytes | number \| null | n/a | Bytes transferred on the flagged activity. |
| response | trigger.signal.anomaly_score | number \| null | n/a | Overall detector score for the flag. |
| response | trigger.signal.surprise | number \| null | n/a | How far the observed volume departs from what this source produces on its own. |
| response | trigger.signal.mismatch | number \| null | n/a | Behavioral-mismatch magnitude (0..10). |
| response | verdict | object \| null | n/a | Apollo's conclusion. null while running. |
| response | verdict.disposition | "benign" \| "malicious" \| "escalate" | n/a | Apollo's call. |
| response | verdict.confidence | number | n/a | Confidence 0..1 — calibrated on evidence held, not ideal evidence. |
| response | verdict.headline | string | n/a | One- to two-sentence plain-language answer. |
| response | verdict.rationale | string | n/a | Fuller reasoning, 1–3 paragraphs. |
| response | at_a_glance | object \| null | n/a | Known / unknown / therefore syllogism. null while running. |
| response | at_a_glance.known | string | n/a | The proven core, one sentence. |
| response | at_a_glance.unknown | string \| null | n/a | The single decisive unresolved fact. null when the verdict is clean. |
| response | at_a_glance.therefore | string | n/a | The call plus the single most important next move. |
| response | key_findings[] | object[] | n/a | The observations that drove the verdict. |
| response | key_findings[].headline | string | n/a | The scannable takeaway. |
| response | key_findings[].observation | string | n/a | What was observed — specific and traceable. |
| response | key_findings[].significance | string | n/a | Why it matters. |
| response | key_findings[].role | "decisive" \| "supporting" \| "ambiguous" \| "context" \| "data_gap" | n/a | Epistemic role of the finding. |
| response | key_findings[].importance | "high" \| "medium" \| "low" | n/a | Relative weight of the finding. |
| response | key_findings[].rules_out | string[] | n/a | Candidate explanations this finding rules out (resolved names). |
| response | key_findings[].confirms | string[] | n/a | Candidate explanations this finding confirms (resolved names). |
| response | key_findings[].timeline[] | object[] \| null | n/a | Temporal anchors: { at, label, children? }. |
| response | key_findings[].traffic_links[] | object[] \| null | n/a | Replayable pointers into the underlying flows. |
| response | key_findings[].traffic_links[].label | string | n/a | Chip text, e.g. "April 9 upload flows (78 flows)". |
| response | key_findings[].traffic_links[].rationale | string \| null | n/a | Why this query demonstrates the finding. |
| response | key_findings[].traffic_links[].query | object \| null | n/a | The internal search query (tenant fields stripped). Opaque/unstable in v1. |
| response | provenance | object \| null | n/a | Access→exfiltration lineage. null when the source host collects no host-logs. |
| response | provenance.available | boolean | n/a | Whether a chain was reconstructed. When false, render the ceiling not an empty chain. |
| response | provenance.unavailable_reason | string \| null | n/a | Why no chain (e.g. host collects no host-logs). |
| response | provenance.headline | string \| null | n/a | One-line narrative of the whole chain. |
| response | provenance.host | string \| null | n/a | Resolved source hostname. |
| response | provenance.completeness | number | n/a | Fraction of legs directly observed (0..1). |
| response | provenance.host_value | object \| null | n/a | What the host is worth to an attacker: { role, value, reach }. |
| response | provenance.host_value.value | "low" \| "moderate" \| "high" | n/a | Attacker value of the host. |
| response | provenance.legs[] | object[] | n/a | Ordered kill-chain steps. |
| response | provenance.legs[].stage | "access" \| "identity" \| "execution" \| "collection" \| "exfiltration" \| "lateral" | n/a | Kill-chain tactic bucket. |
| response | provenance.legs[].title | string | n/a | Display heading, e.g. "Exfiltration". |
| response | provenance.legs[].headline | string | n/a | One-line leg takeaway. |
| response | provenance.legs[].detail | string \| null | n/a | Longer detail for the leg. |
| response | provenance.legs[].confidence | "directly_observed" \| "inferred" \| "not_established" | n/a | Evidentiary confidence for the leg. |
| response | provenance.legs[].gap_reason | string \| null | n/a | Why the link is not directly observed (when confidence ≠ directly_observed). |
| response | provenance.legs[].at | string \| null | n/a | When (ISO-8601 or window string). |
| response | provenance.legs[].actors[] | object[] | n/a | The who/what on this leg: { kind, label, detail? }. |
| response | provenance.legs[].actors[].kind | "human" \| "account" \| "process" \| "source_ip" \| "tool" \| "path" \| "destination" | n/a | Actor type. |
| response | provenance.legs[].attack[] | object[] | n/a | MITRE ATT&CK techniques: { tactic, technique_id, technique }. |
| response | provenance.legs[].pivot_keys | string[] | n/a | Join keys linking this leg to adjacent ones, e.g. ["uid=1002"]. |
| response | decision_support.confidence_limits | string \| null | n/a | The honest cap on certainty given available telemetry. |
| response | decision_support.what_would_change_the_verdict | string \| null | n/a | The single artifact that would flip the verdict. |
| response | decision_support.open_questions[] | object[] | n/a | Unresolved questions: { question, needed_data?, priority? }. |
| response | decision_support.open_questions[].priority | "highest" \| "high" \| "medium" \| "low" | n/a | Priority of the question. |
| response | decision_support.recommended_actions[] | object[] | n/a | Next steps: { timeframe, action }. |
| response | meta.generated_at | string \| null | n/a | When this analysis was produced. |
| response | meta.ai_generated | true | n/a | Always true — the analysis is AI-authored. |
| response | meta.analysis_version | string \| null | n/a | Opaque version of the analysis format. |

#### Python SDK

Method: `prophet.investigations.get(investigation_id: str) -> Investigation | None`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | investigation_id | str | yes | Investigation id. The SDK raises ValidationError when empty. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Investigation \| None | n/a | None when the API responds 404 (missing, not yours, or still running). |
| response | Investigation.verdict | Verdict \| None | n/a | disposition, confidence, headline, rationale. |
| response | Investigation.key_findings | list[KeyFinding] | n/a | headline, observation, significance, role, rules_out, confirms, traffic_links. |
| response | Investigation.provenance | Provenance \| None | n/a | Access→exfiltration legs with actors + MITRE attack refs. |
| response | Investigation.decision_support | DecisionSupport | n/a | confidence_limits, what_would_change_the_verdict, open_questions, recommended_actions. |
| response | Investigation.is_malicious / .needs_escalation / .has_provenance | bool | n/a | Convenience checks. |

SDK example:

```python
inv = prophet.investigations.get("inv_43d67a...")

if inv is None:
    print("not found or not yet complete")
elif inv.needs_escalation:
    print(inv.at_a_glance.therefore)
    for f in inv.key_findings:
        print(f.role, f.headline, "— rules out:", f.rules_out)
    if inv.has_provenance:
        for leg in inv.provenance.legs:
            print(leg.stage, leg.headline, leg.confidence)
```

REST example:

```bash
curl https://app.prophet.io/rest/investigations/1.0/inv_43d67a... \
  -H "Authorization: Bearer $PROPHET_TOKEN"
```

Example response:

```json
{
  "id": "inv_43d67a...",
  "status": "completed",
  "trigger": {
    "source": "10.90.8.16",
    "destination": "google",
    "detected_at": "2026-06-29T20:00:00Z",
    "signal": { "volume_bytes": 616433024, "anomaly_score": 1, "surprise": 2.74, "mismatch": 4.7 }
  },
  "verdict": {
    "disposition": "escalate",
    "confidence": 0.85,
    "headline": "A first-time 616 MB export to Google Drive under valid credentials; intent unresolvable from telemetry.",
    "rationale": "..."
  },
  "at_a_glance": {
    "known": "prophetadmin staged flows.ndjson and pushed ~616 MB to Google Drive — a first-ever export, no escalation or persistence.",
    "unknown": "Whether the export was authorized — no change-management record exists to confirm it.",
    "therefore": "Escalate for a same-day human authorization check."
  },
  "key_findings": [
    {
      "headline": "First-ever rclone-to-Drive export by this user",
      "observation": "Zero prior rclone transfers to gdrive across 179 days; the only copyto is the flagged event.",
      "significance": "No routine precedent, so it cannot be explained as a recurring sync.",
      "role": "decisive",
      "importance": "high",
      "rules_out": ["Legitimate recurring routine"],
      "confirms": [],
      "traffic_links": [
        { "label": "Daily outbound bytes to Google (179 days)", "rationale": "Flat baseline with one spike.", "query": { "query": "src.ip eq 10.90.8.16 and dst.ctx.organization eq google", "config": { "module": "timeseries" } } }
      ]
    }
  ],
  "provenance": {
    "available": true,
    "host": "marketing1",
    "completeness": 0.8,
    "legs": [
      {
        "stage": "exfiltration",
        "headline": "rclone pushed ~616 MB to Google Drive over HTTPS.",
        "confidence": "directly_observed",
        "actors": [{ "kind": "process", "label": "rclone (pid 21210)" }],
        "attack": [{ "tactic": "Exfiltration", "technique_id": "T1567.002", "technique": "Exfil to Cloud Storage" }]
      }
    ]
  },
  "decision_support": {
    "confidence_limits": "Network telemetry cannot see file contents or a backup manifest.",
    "what_would_change_the_verdict": "A Workspace admin-log entry tying this to an approved backup would flip it to benign.",
    "open_questions": [{ "question": "Is there an approved backup job?", "needed_data": "Google Workspace Admin Audit logs", "priority": "high" }],
    "recommended_actions": [{ "timeframe": "within_4h", "action": "Confirm with the host owner whether this was sanctioned." }]
  },
  "meta": { "generated_at": "2026-06-30T23:01:09Z", "ai_generated": true, "analysis_version": "v2" }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 404 | not_found | Investigation does not exist, is not yours, has not finished yet, or the upstream lookup failed. |
| 503 | service_unavailable | No engine is available for the tenant. |
| 504 | upstream_timeout | Upstream request timed out (30s). |

#### SDK notes

- A running investigation has no summary yet — get() returns None until it completes.
- traffic_links[].query is the internal search query passed through with tenant fields stripped; opaque/unstable in v1.

## Explore

External-organization communication shape: which external services a network sends traffic to, and the texture of each relationship (when, rhythm, transfer, who, how). Communication shape is what Prophet models to detect breaches — these endpoints expose the same view of your network for exploration.

### GET /rest/explore/1.0/egress/organizations — List external organizations

Docs URL: https://docs.prophet.io/#api-explore-egress-organizations
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

External organizations the network sent traffic to, ranked by volume and merged across instance_ids.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | instance_ids | string[] | yes | Instances/customer IDs to aggregate over (merged). Self or authorized children. |
| body | start | DateTimeFilter | no | Start time filter. Defaults to relative 24 hours. |
| body | end | DateTimeFilter | no | End time filter. Defaults to now. |
| body | size | number | no | Maximum organizations to return. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | organizations[].name | string | n/a | Organization name (dst.ctx.organization). |
| response | organizations[].industry | string \| null | n/a | Classified industry for the organization. |
| response | organizations[].apps[] | string[] | n/a | Up to three top application labels by flow count. |
| response | organizations[].bytes | number | n/a | Total bytes (upload + download). |
| response | organizations[].upload | number | n/a | Egress bytes (network -> org). |
| response | organizations[].download | number | n/a | Ingress bytes (org -> network). |
| response | organizations[].flows | number | n/a | Flow records in the window. |
| response | organizations[].sources | number | n/a | Distinct internal hosts talking to the org. |
| response | total_orgs | number | n/a | Distinct external organizations in the window. |
| response | coverage.classified_in_page | number | n/a | Flows behind the returned orgs. Org classification covers a minority of flows. |
| response | coverage.total_flows | number | n/a | Total flows in the window. |
| response | base_pql | string | n/a | PQL fragment scoping the egress view; combine it with your own predicates in the Search API. |
| response | results_for[] | string[] | n/a | Instance IDs the merged data covers. |
| response | status | string | n/a | success on 200; failure with ui_message when the query fails. |

#### Python SDK

Method: `prophet.explore.egress.organizations(instances: str | list[str], *, start: TimeFilter | None = None, end: TimeFilter | None = None, size: int = 25) -> OrganizationList`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | A customer ID or list of IDs. Sent as instance_ids. |
| response | start | TimeFilter \| None | no | Now, MinutesAgo, HoursAgo, DaysAgo, WeeksAgo, or At. |
| response | end | TimeFilter \| None | no | End time filter. |
| response | size | int | no | Max organizations. Default 25. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | OrganizationList | n/a | Typed result: .organizations, .total_orgs, .coverage, .results_for. |
| response | OrganizationRow | model | n/a | name, bytes, upload, download, flows, sources. |

SDK example:

```python
from prophet.sdk import HoursAgo, Now

orgs = prophet.explore.egress.organizations("acme_msp", start=HoursAgo(24))
for o in orgs.organizations:
    print(o.name, o.upload, o.download, o.sources)
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_ids": ["acme_msp-d7890e123"],
    "start": { "relative": { "value": 24, "unit": "hours" } },
    "end": { "now": true },
    "size": 25
  }'
```

Example response:

```json
{
  "organizations": [
    { "name": "mongodb", "industry": "technology", "apps": ["MongoDB"], "bytes": 131448400959, "upload": 115369237055, "download": 15979163904, "flows": 1045864, "sources": 994 }
  ],
  "total_orgs": 45,
  "coverage": { "classified_in_page": 1217928, "total_flows": 73961652 },
  "base_pql": "src.address_type eq private and dst.address_type eq public and dst.ctx.organization ex",
  "status": "success",
  "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

#### SDK notes

- Results are merged across instances; results_for echoes the covered scope.

### GET /rest/explore/1.0/egress/organizations/:org — Organization header

Docs URL: https://docs.prophet.io/#api-explore-egress-organization
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

Stable header attributes for one organization: geo, industry, host and endpoint counts, processes, and a plain-language readout.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | org | string | yes | Organization name (dst.ctx.organization). |
| body | instance_ids | string[] | yes | Instances/customer IDs to aggregate over. |
| body | start | DateTimeFilter | no | Start time filter. Defaults to relative 24 hours. |
| body | end | DateTimeFilter | no | End time filter. Defaults to now. |
| body | src_ip | string | no | Scope to one internal host -> org relationship. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | org | string | n/a | Organization name. |
| response | industry | string \| null | n/a | Classified industry. |
| response | geo | string \| null | n/a | Top destination country. |
| response | upload / download | number | n/a | Egress / ingress bytes. |
| response | sources / endpoints | number | n/a | Distinct internal hosts / org endpoints. |
| response | processes[] | string[] | n/a | Top internal processes reaching the org (packet-sourced). |
| response | readout | string | n/a | Plain-language summary. |
| response | results_for[] | string[] | n/a | Instance IDs covered. |

#### Python SDK

Method: `prophet.explore.egress.organization(instances: str | list[str], org: str, *, start: TimeFilter | None = None, end: TimeFilter | None = None, src_ip: str | None = None) -> OrganizationHeader`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | Customer ID or list. |
| response | org | str | yes | Organization name. |
| response | src_ip | str \| None | no | Scope to one host. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | OrganizationHeader | n/a | org, industry, geo, upload, download, sources, endpoints, processes, readout. |

SDK example:

```python
header = prophet.explore.egress.organization("acme_msp", "google")
print(header.geo, header.industry, header.processes, header.readout)
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations/google \
  -H "Authorization: Bearer $PROPHET_TOKEN" -H "Content-Type: application/json" \
  -d '{"instance_ids":["acme_msp-d7890e123"]}'
```

Example response:

```json
{
  "org": "google", "industry": "technology", "geo": "United States",
  "upload": 32284038, "download": 632326453, "sources": 88, "endpoints": 1,
  "processes": ["python", "python3"],
  "readout": "88 internal host(s) -> 1 endpoint(s); driven by python",
  "status": "success", "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

### GET /rest/explore/1.0/egress/organizations/:org/temporal — Organization — temporal

Docs URL: https://docs.prophet.io/#api-explore-egress-temporal
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

WHEN: a day-of-week by hour-of-day heatmap of traffic to the organization.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | org | string | yes | Organization name. |
| body | instance_ids | string[] | yes | Instances to aggregate over. |
| body | start | DateTimeFilter | no | Start time. Defaults to relative 7 days. |
| body | end | DateTimeFilter | no | End time. Defaults to now. |
| body | src_ip | string | no | Scope to one host. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | cells[].row | string | n/a | Day-of-week label (Mon..Sun). |
| response | cells[].col | string | n/a | Hour-of-day label (00..23). |
| response | cells[].value | number | n/a | Flow count for that day/hour cell. |
| response | rows | string[] | n/a | Ordered day labels (Mon..Sun). |
| response | cols | string[] | n/a | Ordered hour labels (00..23). |
| response | total | number | n/a | Total flows in the window. |
| response | readout | string | n/a | e.g. "always-on — traffic in nearly every hour (automated)". |

#### Python SDK

Method: `prophet.explore.egress.temporal(instances, org, *, start=None, end=None, src_ip=None) -> Temporal`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | Customer ID or list. |
| response | org | str | yes | Organization name. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Temporal | n/a | .cells, .rows, .cols, .total, .readout. |

SDK example:

```python
t = prophet.explore.egress.temporal("acme_msp", "google", start=HoursAgo(168))
print(t.total, t.readout)
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations/google/temporal \
  -H "Authorization: Bearer $PROPHET_TOKEN" -H "Content-Type: application/json" \
  -d '{"instance_ids":["acme_msp-d7890e123"],"start":{"relative":{"value":168,"unit":"hours"}},"end":{"now":true}}'
```

Example response:

```json
{
  "cells": [{ "row": "Mon", "col": "09", "value": 240 }],
  "rows": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
  "cols": ["00", "01", "02"],
  "total": 55286,
  "readout": "always-on — traffic in nearly every hour (automated)",
  "status": "success", "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

### GET /rest/explore/1.0/egress/organizations/:org/cadence — Organization — cadence

Docs URL: https://docs.prophet.io/#api-explore-egress-cadence
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

RHYTHM: is the communication consistent or random? The session-gap CDF (stepped = machine-driven) plus the beacon-flagged subset.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | org | string | yes | Organization name. |
| body | instance_ids | string[] | yes | Instances to aggregate over. |
| body | start | DateTimeFilter | no | Start time. Defaults to relative 24 hours. |
| body | end | DateTimeFilter | no | End time. Defaults to now. |
| body | src_ip | string | no | Scope to one internal host -> org relationship. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | cdf[].gap_secs | number | n/a | Session-gap value at this point of the CDF, in seconds. |
| response | cdf[].fraction | number | n/a | Fraction of sessions with a gap at or below gap_secs (0..1). |
| response | dominant_intervals[] | { gap_secs, fraction }[] | n/a | Up to three dominant beats — vertical steps of the CDF holding at least 15% of the mass. A strong beat means a scheduler owns the relationship. |
| response | steppedness | number \| null | n/a | Mass fraction of the largest beat (~1.0 means one interval dominates: machine-driven). |
| response | median_gap_secs | number \| null | n/a | Median session gap in seconds. |
| response | stats | object | n/a | Map of metric name -> value: mean regularity, mean_gap (seconds), and burstiness. |
| response | beacon.flows | number | n/a | Beacon-flagged (periodic) flow count. |
| response | beacon.sources | number | n/a | Distinct sources with beacon-flagged flows. |
| response | beacon.interval_ms | number \| null | n/a | Mean detected beacon interval in milliseconds. |
| response | readout | string | n/a | e.g. "steady rhythm, ~60 s between flows". |

#### Python SDK

Method: `prophet.explore.egress.cadence(instances, org, *, start=None, end=None, src_ip=None) -> Cadence`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | Customer ID or list. |
| response | org | str | yes | Organization name. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Cadence | n/a | .cdf, .dominant_intervals, .steppedness, .median_gap_secs, .stats, .beacon, .readout. |

SDK example:

```python
c = prophet.explore.egress.cadence("acme_msp", "google")
print(c.beacon, c.readout)
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations/google/cadence \
  -H "Authorization: Bearer $PROPHET_TOKEN" -H "Content-Type: application/json" \
  -d '{"instance_ids":["acme_msp-d7890e123"]}'
```

Example response:

```json
{
  "cdf": [{ "gap_secs": 0.62, "fraction": 0.05 }, { "gap_secs": 26.5, "fraction": 0.5 }, { "gap_secs": 61.2, "fraction": 0.95 }],
  "dominant_intervals": [{ "gap_secs": 60.0, "fraction": 0.42 }],
  "steppedness": 0.42,
  "median_gap_secs": 26.5,
  "stats": { "stats.timing.inter_arrival_secs.regularity": 10.2, "stats.timing.inter_arrival_secs.mean_gap": 26.5 },
  "beacon": { "flows": 24, "sources": 4, "interval_ms": 19.9 },
  "readout": "bursty / irregular, ~26.5 s between flows, 24 beacon-like flows from 4 sources",
  "status": "success", "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

### GET /rest/explore/1.0/egress/organizations/:org/transfer — Organization — transfer

Docs URL: https://docs.prophet.io/#api-explore-egress-transfer
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

WHAT IS MOVED: chunk size (mean and coefficient of variation), payload entropy, session duration, and upload/download split.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | org | string | yes | Organization name. |
| body | instance_ids | string[] | yes | Instances to aggregate over. |
| body | start | DateTimeFilter | no | Start time. Defaults to relative 24 hours. |
| body | end | DateTimeFilter | no | End time. Defaults to now. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | transfer.chunk_bytes_mean | number | n/a | Mean packet/chunk size (packet-sourced). |
| response | transfer.chunk_cv | number | n/a | Coefficient of variation — low is uniform (bulk sync), high is mixed. |
| response | transfer.entropy_mean | number \| null | n/a | Mean payload entropy (encrypted/compressed when high). |
| response | transfer.duration_mean | number \| null | n/a | Mean session duration in seconds. |
| response | transfer.up / transfer.down | number | n/a | Egress / ingress bytes. |
| response | readout | string | n/a | e.g. "~64 KB uniform chunks, encrypted, upload-heavy". |

#### Python SDK

Method: `prophet.explore.egress.transfer(instances, org, *, start=None, end=None, src_ip=None) -> Transfer`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | Customer ID or list. |
| response | org | str | yes | Organization name. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Transfer | n/a | .transfer (chunk_bytes_mean, chunk_cv, entropy_mean, duration_mean, up, down), .readout. |

SDK example:

```python
x = prophet.explore.egress.transfer("acme_msp", "mongodb")
print(x.transfer.chunk_bytes_mean, x.transfer.chunk_cv, x.readout)
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations/mongodb/transfer \
  -H "Authorization: Bearer $PROPHET_TOKEN" -H "Content-Type: application/json" \
  -d '{"instance_ids":["acme_msp-d7890e123"]}'
```

Example response:

```json
{
  "transfer": { "chunk_bytes_mean": 681.3, "chunk_cv": 0.80, "entropy_mean": 6.84, "duration_mean": 11.3, "up": 114882003430, "down": 16013469796 },
  "readout": "~681 B mixed-size chunks, 11.3 s sessions, encrypted/compressed payloads, upload-heavy",
  "status": "success", "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

#### SDK notes

- chunk_bytes_mean / chunk_cv describe packet-level chunking; entropy is measured on packet payloads.

### GET /rest/explore/1.0/egress/organizations/:org/reach — Organization — reach

Docs URL: https://docs.prophet.io/#api-explore-egress-reach
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

WHO / WHAT: the internal sources and processes reaching the organization.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | org | string | yes | Organization name. |
| body | instance_ids | string[] | yes | Instances to aggregate over. |
| body | start | DateTimeFilter | no | Start time. Defaults to relative 24 hours. |
| body | end | DateTimeFilter | no | End time. Defaults to now. |
| body | size | number | no | Maximum sources to return. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | sources[].ip | string | n/a | Internal host IP. |
| response | sources[].upload | number | n/a | Egress bytes from this host to the org. |
| response | sources[].download | number | n/a | Ingress bytes from the org to this host. |
| response | sources[].flows | number | n/a | Flow records for this host. |
| response | processes[].name | string | n/a | Process name (packet-sourced eBPF attribution). |
| response | processes[].flows | number | n/a | Flow records attributed to the process. |
| response | processes[].sources | number | n/a | Distinct internal hosts running the process. |
| response | source_count | number | n/a | Distinct internal hosts. |
| response | readout | string | n/a | e.g. "26 internal host(s) -> 25 endpoint(s); driven by jumpcloud-agent". |

#### Python SDK

Method: `prophet.explore.egress.reach(instances, org, *, start=None, end=None, src_ip=None, size=50) -> Reach`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | Customer ID or list. |
| response | org | str | yes | Organization name. |
| response | size | int | no | Max sources. Default 50. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Reach | n/a | .sources, .processes, .source_count, .readout. |

SDK example:

```python
r = prophet.explore.egress.reach("acme_msp", "jumpcloud")
for s in r.sources:
    print(s.ip, s.upload, s.flows)
print([p.name for p in r.processes])
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations/jumpcloud/reach \
  -H "Authorization: Bearer $PROPHET_TOKEN" -H "Content-Type: application/json" \
  -d '{"instance_ids":["acme_msp-d7890e123"]}'
```

Example response:

```json
{
  "sources": [{ "ip": "10.90.77.1", "upload": 2852885, "download": 2013571, "flows": 622 }],
  "processes": [{ "name": "jumpcloud-agent", "flows": 4030, "sources": 26 }],
  "source_count": 26,
  "readout": "26 internal host(s) -> 26 endpoint(s); driven by jumpcloud-agent",
  "status": "success", "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

#### SDK notes

- Add src_ip to any dimension to scope the whole fingerprint to one host -> org relationship.

### GET /rest/explore/1.0/egress/organizations/:org/access — Organization — access

Docs URL: https://docs.prophet.io/#api-explore-egress-access
Family: Explore
Auth: bearer token
Scope: p.token.scope.explore_api
Version: 1.0
Stability: stable

HOW REACHED: TLS versions, ports, protocols, and applications used to reach the organization.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | org | string | yes | Organization name. |
| body | instance_ids | string[] | yes | Instances to aggregate over. |
| body | start | DateTimeFilter | no | Start time. Defaults to relative 24 hours. |
| body | end | DateTimeFilter | no | End time. Defaults to now. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | tls_versions[].name | string | n/a | TLS version label (e.g. TLS 1.3). |
| response | tls_versions[].flows | number | n/a | Flow records at this TLS version. |
| response | tls_versions[].bytes | number | n/a | Total bytes at this TLS version. |
| response | ports[] | { name, flows, bytes }[] | n/a | Destination ports. Same row shape as tls_versions[]. |
| response | protocols[] | { name, flows, bytes }[] | n/a | Transport protocols. Same row shape as tls_versions[]. |
| response | apps[] | { name, flows, bytes }[] | n/a | Application labels from protocol classification. Same row shape as tls_versions[]. |
| response | readout | string | n/a | e.g. "TLS 1.3 / port 443 / HTTPS.Google". |

#### Python SDK

Method: `prophet.explore.egress.access(instances, org, *, start=None, end=None, src_ip=None) -> Access`

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | instances | str \| list[str] | yes | Customer ID or list. |
| response | org | str | yes | Organization name. |

SDK outputs:

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | return | Access | n/a | .tls_versions, .ports, .protocols, .apps, .readout. |

SDK example:

```python
a = prophet.explore.egress.access("acme_msp", "google")
print([(v.name, v.flows) for v in a.tls_versions], a.readout)
```

REST example:

```bash
curl -X GET https://app.prophet.io/rest/explore/1.0/egress/organizations/google/access \
  -H "Authorization: Bearer $PROPHET_TOKEN" -H "Content-Type: application/json" \
  -d '{"instance_ids":["acme_msp-d7890e123"]}'
```

Example response:

```json
{
  "tls_versions": [{ "name": "TLS 1.3", "flows": 1163, "bytes": 9600000 }, { "name": "TLS 1.0", "flows": 114 }],
  "ports": [{ "name": "443", "flows": 6607 }],
  "protocols": [{ "name": "TCP", "flows": 6607 }],
  "apps": [{ "name": "HTTPS.Google", "flows": 6313 }],
  "readout": "TLS 1.3 / port 443 / HTTPS.Google",
  "status": "success", "results_for": ["acme_msp-d7890e123"]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |

## Events

Fetch event topics and records from Prophet plugin activity.

### GET /rest/events/1.0/:customerId — Fetch event topics

Docs URL: https://docs.prophet.io/#api-events-topics
Family: Events
Auth: bearer token
Scope: p.token.scope.event_api
Version: 1.0
Stability: stable

Fetches plugin event topics for a customer.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | customerId | string | yes | Tenant/customer ID whose event topics should be fetched. |
| body | start | DateFilter | no | Start date filter. Defaults to relative 15 minutes. |
| body | end | DateFilter | no | End date filter. Defaults to now. |
| body | modules | string[] | no | Module filter. Omit to apply no module filter. |
| body | states | string[] | no | Event state filter. |
| body | plugins | string[] | no | Plugin name filter. |
| body | labels | string[] | no | Label filter applied to event records. |
| body | sort | { field: string, order: "asc" \| "desc" }[] | no | Sort entries applied to the result set. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | operation | "fetch_topics" | n/a | Operation echoed in the response. |
| response | status | string | n/a | Request status, usually success. |
| response | events[] | object[] | n/a | Event topic summaries returned as notifications. |
| response | returned | number | n/a | Number of topics returned. |

REST example:

```bash
curl -X GET https://app.prophet.io/rest/events/1.0/acme_msp-d7890e123 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "start": { "relative": { "value": 24, "unit": "hours" } },
    "end": { "now": true },
    "modules": ["infrastructure"],
    "states": ["active"],
    "sort": [{ "field": "@timestamp", "order": "desc" }]
  }'
```

Example response:

```json
{
  "operation": "fetch_topics",
  "status": "success",
  "events": [
    {
      "id": "evt-123",
      "module": "infrastructure",
      "plugin": "zeek",
      "state": "active",
      "labels": ["prod"]
    }
  ],
  "returned": 1
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 403 | cross_instance_authorization | :customerId is outside the caller tenant tree. |
| 500 | compute_engine_error | Upstream request failed. |

### GET /rest/events/1.0/:customerId/:eventId — Fetch event records

Docs URL: https://docs.prophet.io/#api-events-records
Family: Events
Auth: bearer token
Scope: p.token.scope.event_api
Version: 1.0
Stability: stable

Fetches event records for one event topic.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| path | customerId | string | yes | Tenant/customer ID whose records should be fetched. |
| path | eventId | string | yes | Event topic identifier. |
| body | start | DateFilter | no | Start date filter. Defaults to relative 15 minutes. |
| body | end | DateFilter | no | End date filter. Defaults to now. |
| body | sort | { field: string, order: "asc" \| "desc" }[] | no | Sort entries applied to the result set. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | page_size | number \| null | n/a | Page size from searchProps when returned by the engine. |
| response | page_next | string \| number \| null | n/a | Next page cursor from searchProps when returned by the engine. |
| response | returned | number | n/a | Number of event records returned. |
| response | operation | "fetch_records" | n/a | Operation echoed in the response. |
| response | status | string | n/a | Request status, usually success. |
| response | parts[] | object[] | n/a | Event record rows for the topic. |

REST example:

```bash
curl -X GET https://app.prophet.io/rest/events/1.0/acme_msp-d7890e123/evt-123 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "start": { "relative": { "value": 24, "unit": "hours" } },
    "end": { "now": true },
    "sort": [{ "field": "@timestamp", "order": "desc" }]
  }'
```

Example response:

```json
{
  "page_size": 100,
  "page_next": "cursor-2",
  "returned": 1,
  "operation": "fetch_records",
  "status": "success",
  "parts": [
    {
      "id": "part-123",
      "event_id": "evt-123",
      "@timestamp": "2026-02-04T16:00:00.000Z"
    }
  ]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 403 | cross_instance_authorization | :customerId is outside the caller tenant tree. |
| 500 | compute_engine_error | Upstream request failed. |

## Automation

List, set, and delete automations scoped to a tenant or child tenant.

### GET /rest/automation/1.0 — List or fetch automations

Docs URL: https://docs.prophet.io/#api-automation-list
Family: Automation
Auth: bearer token
Scope: p.token.scope.automation_api
Version: 1.0
Stability: stable

Lists automations or fetches one automation when both id and feed_id are supplied.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | instance_id | string | yes | Target tenant/customer ID. |
| body | filter | string | no | Text filter forwarded as textFilter to the automation engine. |
| body | id | string | no | Automation ID. Include feed_id with id to fetch one automation. |
| body | feed_id | string | no | Automation feed. Defaults to local; required with id when fetching one automation. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | "success" \| "error" \| string | n/a | Automation engine status. |
| response | operation | "list" \| "fetch" | n/a | list when id is omitted, fetch when id and feed_id are supplied. |
| response | automations[] | object[] \| omitted | n/a | Declarative automations for list responses. |
| response | automation | object \| omitted | n/a | Declarative automation for fetch responses. |
| response | message | string \| omitted | n/a | Error or informational message when returned by the engine. |

REST example:

```bash
curl -X GET https://app.prophet.io/rest/automation/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_id": "acme_msp-d7890e123",
    "filter": "dns"
  }'
```

Example response:

```json
{
  "status": "success",
  "operation": "list",
  "automations": [
    {
      "id": "auto-123",
      "feed_id": "local",
      "name": "DNS exfiltration guard",
      "tags": ["dns"],
      "enabled": true
    }
  ]
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 400 | body.validation | id and feed_id must be supplied together when fetching one automation. |
| 403 | cross_instance_authorization | instance_id is missing or outside the caller tenant tree. |

### POST /rest/automation/1.0 — Create or update automation

Docs URL: https://docs.prophet.io/#api-automation-set
Family: Automation
Auth: bearer token
Scope: p.token.scope.automation_api
Version: 1.0
Stability: stable

Creates or updates an automation pipeline for a tenant.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | instance_id | string | yes | Target tenant/customer ID. |
| body | name | string | yes | Automation display name. |
| body | pipeline | object[] \| object | yes | Declarative automation pipeline forwarded to the automation engine. |
| body | tags | string[] | no | Tags stored with the automation. Defaults to an empty list. |
| body | feed_id | string | no | Automation feed. Defaults to local. |
| body | id | string | no | Automation ID. Defaults to a generated UUID for creates. |
| body | enabled | boolean | no | Enabled state. Controller coerces with Boolean(enabled). |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | "success" \| "error" \| string | n/a | Automation engine status. |
| response | operation | "set" | n/a | Create/update operation. |
| response | automation | object | n/a | Saved declarative automation returned by the automation engine. |
| response | message | string \| omitted | n/a | Error message when the engine reports status error. |
| response | customer_id | string \| omitted | n/a | Customer ID included by the engine on errors. |

REST example:

```bash
curl -X POST https://app.prophet.io/rest/automation/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_id": "acme_msp-d7890e123",
    "name": "DNS exfiltration guard",
    "pipeline": [{ "type": "match", "field": "dst.port", "operator": "eq", "value": 53 }],
    "tags": ["dns"],
    "enabled": true
  }'
```

Example response:

```json
{
  "status": "success",
  "operation": "set",
  "automation": {
    "id": "auto-123",
    "feed_id": "local",
    "name": "DNS exfiltration guard",
    "tags": ["dns"],
    "enabled": true
  }
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 403 | cross_instance_authorization | instance_id is missing or outside the caller tenant tree. |

### DELETE /rest/automation/1.0 — Delete automation

Docs URL: https://docs.prophet.io/#api-automation-delete
Family: Automation
Auth: bearer token
Scope: p.token.scope.automation_api
Version: 1.0
Stability: stable

Deletes an automation by ID for the scoped instance.

#### API inputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| body | instance_id | string | yes | Target tenant/customer ID. |
| body | id | string | yes | Automation ID to delete. |

#### API outputs

| Location | Name | Type | Required | Description |
| --- | --- | --- | --- | --- |
| response | status | "success" \| "info" \| "error" \| string | n/a | success when deleted, info when the automation does not exist, or error from the engine. |
| response | operation | "delete" | n/a | Delete operation. |
| response | message | string \| omitted | n/a | Informational or error message returned by the engine. |
| response | customer_id | string \| omitted | n/a | Customer ID included by the engine on errors. |

REST example:

```bash
curl -X DELETE https://app.prophet.io/rest/automation/1.0 \
  -H "Authorization: Bearer $PROPHET_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_id": "acme_msp-d7890e123",
    "id": "auto-123"
  }'
```

Example response:

```json
{
  "status": "success",
  "operation": "delete"
}
```

#### Errors

| Status | Code | Description |
| --- | --- | --- |
| 401 | authentication_error | Missing, expired, or invalid bearer token. |
| 403 | authorization_error | Token lacks the required scope or tenant access. |
| 403 | cross_instance_authorization | instance_id is missing or outside the caller tenant tree. |

