API v1.1.0

Developer Documentation

Integrate SURGE forensic investigation data into your SIEM, SOAR, and security workflows. Full REST API with webhook event streaming.

Quick Start

The SURGE API provides programmatic access to your forensic investigation data. All requests are authenticated via API key and scoped to your tenant.

1

Create an API key

Go to Settings → API Keys in the SURGE dashboard and create a new key. Copy it immediately — it's only shown once.

2

Make your first request

Verify connectivity with the health check endpoint:

bash
curl -s https://app.surge.security/api/v1/ping \
  -H "X-API-Key: surge_your_key_here" | jq .
json
{
  "status": "ok",
  "api_version": "1.1.0"
}
3

Fetch your investigations

List completed investigations with optional filters:

bash
curl -s https://app.surge.security/api/v1/investigations \
  -H "X-API-Key: surge_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"status":"complete","per_page":5}' | jq .

Authentication

All API requests require an X-API-Key header. Keys are managed via the Settings page in the SURGE dashboard.

API access requires a Professional plan or above. Keys created on lower tiers will receive a 403 response.

Key Format

API keys follow the format surge_<64 hex characters>. The first 8 characters serve as a non-secret prefix for key identification.

bash
# Include the key in every request
curl https://app.surge.security/api/v1/ping \
  -H "X-API-Key: surge_a1b2c3d4e5f6..."

Scopes

Each key is assigned one or more scopes that control access:

Scope Permissions
read List and retrieve investigations, findings, endpoints, sigma rules
write Trigger new collections/investigations via the API

Key Management

Manage API keys via the SURGE dashboard under Settings → API Keys. You can:

  • Create new keys with specific name and scopes
  • List all active keys (prefix and metadata only)
  • Revoke keys immediately — revoked keys cannot be reactivated

API Reference

Base URL: https://app.surge.security/api/v1

Health & Meta

GET /api/v1/ping scope: read

Health check and authentication validation. Use this to verify your API key is working.

Response
json
{
  "status": "ok",
  "api_version": "1.1.0"
}
GET /api/v1/openapi.json scope: read

OpenAPI 3.x specification for the SURGE API. Use this to auto-generate SDK clients, Postman collections, or SIEM/SOAR connectors.

Investigations

POST /api/v1/investigations scope: read

List investigations with optional filters. Uses POST to support complex filter bodies. Supports incremental polling via timestamp filters.

Body Parameters
Name Type Description
status string Filter by status: "complete" (default) or "archived"
verdict string Filter by verdict: "malicious", "review", or "benign"
severity string Filter by severity: CRITICAL, HIGH, MEDIUM, LOW, INFO
created_after string ISO-8601 timestamp for incremental polling (e.g. "2025-01-01T00:00:00Z")
updated_after string ISO-8601 timestamp — returns investigations updated after this time
page integer Page number (default: 1)
per_page integer Results per page (default: 50, max: 200)
Request
json
{
  "status": "complete",
  "severity": "HIGH",
  "created_after": "2025-01-01T00:00:00Z",
  "page": 1,
  "per_page": 25
}
Response
json
{
  "total": 42,
  "page": 1,
  "per_page": 25,
  "investigations": [
    {
      "investigation_id": "1234",
      "hostname": "WORKSTATION-01",
      "ip_address": "10.0.1.50",
      "operating_system": "windows",
      "verdict": "malicious",
      "confidence": "high",
      "severity": "HIGH",
      "triage_priority": "P1",
      "status": "complete",
      "created_at": "2025-06-15T10:30:00+00:00",
      "updated_at": "2025-06-15T10:45:22+00:00",
      "description": "Forensic analysis of WORKSTATION-01",
      "mitre_techniques": ["T1059.001", "T1053.005", "T1027"]
    }
  ]
}
GET /api/v1/investigations/{investigation_id} scope: read

Get full details for a single investigation, including verdict, MITRE techniques with severity scores, and evidence indicators.

Response
json
{
  "investigation_id": "1234",
  "status": "complete",
  "severity": "HIGH",
  "created_at": "2025-06-15T10:30:00+00:00",
  "updated_at": "2025-06-15T10:45:22+00:00",
  "description": "Forensic analysis of WORKSTATION-01",
  "actual_cost": 1,
  "data": {
    "hostname": "WORKSTATION-01",
    "ip_address": "10.0.1.50",
    "operating_system": "windows",
    "verdict": "malicious",
    "confidence": "high",
    "maliciousness_score": 87,
    "report_classification": "Credential Theft + Lateral Movement",
    "evidence_indicators": ["Mimikatz execution", "PsExec lateral movement"],
    "triage_priority": "P1",
    "recommended_action": "Isolate and investigate",
    "analysis_duration_formatted": "3m 42s",
    "endpoint_type": "workstation",
    "mitre_techniques": {
      "T1059.001": 95,
      "T1053.005": 80,
      "T1027": 70
    }
  }
}
GET /api/v1/investigations/{investigation_id}/status scope: read

Lightweight status poll. Use this to check if an investigation is complete without fetching full details.

Response
json
{
  "investigation_id": "1234",
  "status": "complete",
  "verdict": "malicious",
  "confidence": "high",
  "boost_status": null
}
GET /api/v1/investigations/{investigation_id}/findings scope: read

Get structured findings for an investigation. Each finding includes outcome (signal/noise), MITRE techniques, and a description. Supports CEF output for SIEM ingestion.

Query Parameters
Name Type Description
format string "json" (default) or "cef" (Common Event Format for SIEM ingestion)
Response
json
{
  "investigation_id": "1234",
  "verdict": "malicious",
  "triage_priority": "P1",
  "signal_count": 5,
  "noise_count": 12,
  "findings": [
    {
      "title": "PowerShell Encoded Command Execution",
      "description": "Encoded PowerShell command detected launching Invoke-Mimikatz...",
      "outcome": "signal",
      "status": "confirmed",
      "mitre_techniques": ["T1059.001", "T1027"],
      "severity": "high",
      "evidence": ["Event ID 4104", "ScriptBlock logging"]
    }
  ]
}
GET /api/v1/investigations/{investigation_id}/sigma_rules scope: read

Get auto-generated Sigma detection rules for an investigation. Rules are derived from the attack techniques observed during analysis.

Response
json
{
  "investigation_id": "1234",
  "total_rules": 3,
  "sigma_rules": [
    {
      "title": "Suspicious PowerShell Encoded Execution",
      "status": "experimental",
      "level": "high",
      "logsource": { "category": "process_creation", "product": "windows" },
      "detection": { ... }
    }
  ]
}

Findings (Bulk)

POST /api/v1/findings scope: read

Flat findings list across all investigations, optimized for SIEM bulk ingestion. Each finding includes parent investigation metadata. Supports the same filters as /investigations.

Body Parameters
Name Type Description
status string Filter by investigation status: "complete" (default) or "archived"
verdict string Filter by verdict: "malicious", "review", or "benign"
severity string Filter by severity: CRITICAL, HIGH, MEDIUM, LOW, INFO
created_after string ISO-8601 timestamp for incremental polling
updated_after string ISO-8601 timestamp filter
page integer Page number (default: 1)
per_page integer Results per page (default: 50, max: 200)
Query Parameters
Name Type Description
format string "json" (default) or "cef" (Common Event Format)
Response
json
{
  "total_findings": 127,
  "page": 1,
  "per_page": 50,
  "findings": [
    {
      "investigation_id": "1234",
      "hostname": "WORKSTATION-01",
      "operating_system": "windows",
      "verdict": "malicious",
      "severity": "HIGH",
      "created_at": "2025-06-15T10:30:00+00:00",
      "title": "PowerShell Encoded Command Execution",
      "outcome": "signal",
      "mitre_techniques": ["T1059.001"]
    }
  ]
}

Endpoints

GET /api/v1/endpoints scope: read

List monitored endpoints for your tenant. Supports pagination.

Query Parameters
Name Type Description
page integer Page number (default: 1)
per_page integer Results per page (default: 50, max: 200)
Response
json
{
  "total": 8,
  "page": 1,
  "per_page": 50,
  "endpoints": [
    {
      "id": "42",
      "hostname": "WORKSTATION-01",
      "operating_system": "windows",
      "last_seen_ip": "10.0.1.50",
      "description": "Finance department workstation",
      "created_at": "2025-05-20T08:00:00+00:00"
    }
  ]
}

Collection

POST /api/v1/collect scope: write

Trigger a new investigation programmatically. Creates an endpoint placeholder and returns the upload URL for the collection archive. Requires write scope.

Body Parameters
Name Type Description
hostname required string Endpoint hostname (max 255 characters)
operating_system string "windows" (default), "macos", or "linux"
effort_level string "triage" (default) or "boost" (deep analysis)
Request
json
{
  "hostname": "WORKSTATION-01",
  "operating_system": "windows",
  "effort_level": "triage"
}
Response
json
{
  "status": "ready",
  "message": "Upload the collection archive to the agent_report endpoint.",
  "upload_url": "/endpoints/new",
  "hostname": "WORKSTATION-01",
  "effort_level": "triage",
  "billing_plan": "professional",
  "investigations_remaining": 72
}

Webhooks

Overview

Webhooks push real-time event notifications to your endpoints via HTTP POST. Subscribe to specific events and receive payloads as they occur — no polling required.

Webhooks require a Professional plan or above. Webhook URLs must use HTTPS.

Available Events

Event Description
investigation.completed Investigation finished with verdict and findings
investigation.severity_changed Investigation severity updated (e.g. after boost)
investigation.boosted Investigation upgraded to deep analysis (boost)
endpoint.created New endpoint registered via collector or API
scan.completed Scheduled scan completed
scan.drift_detected Configuration drift detected between scans
scan.missed_run Scheduled scan failed to run
finding.signal Individual signal finding detected (for SIEM alert ingestion)

Managing Webhooks

Create, list, test, and delete webhook subscriptions via the SURGE dashboard or API. Webhook secrets are auto-generated if not provided and are returned only on creation.

bash
# Create a webhook subscription
curl -X POST https://app.surge.security/settings/webhooks \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhook",
    "events": ["investigation.completed", "finding.signal"]
  }'
json
{
  "id": "7",
  "url": "https://your-server.com/webhook",
  "events": ["investigation.completed", "finding.signal"],
  "secret": "a1b2c3d4...your_webhook_secret...e5f6",
  "created_at": "2025-06-15T12:00:00+00:00"
}

Payload Format

Every webhook delivery is an HTTP POST with a JSON body and SURGE-specific headers for verification and routing.

Headers

Header Description
X-Surge-Signature HMAC-SHA256 signature: sha256=<hex>
X-Surge-Timestamp ISO-8601 timestamp of when the payload was signed
X-Surge-Delivery-Id Unique UUID for this delivery attempt (for deduplication)
X-Surge-Event Event type (e.g. investigation.completed)
User-Agent SURGE-Webhooks/1.0

Payload Structure

json
{
  "event": "investigation.completed",
  "timestamp": "2025-06-15T10:45:22+00:00",
  "delivery_id": "d4e5f6a7-b8c9-1234-5678-abcdef012345",
  "data": {
    "investigation_id": "1234",
    "hostname": "WORKSTATION-01",
    "verdict": "malicious",
    "severity": "HIGH",
    "confidence": "high",
    "mitre_techniques": ["T1059.001", "T1053.005"]
  }
}

Event Payload Schemas

investigation.completed

typescript
// data payload
{
  investigation_id: string;
  hostname: string;
  operating_system: string;
  verdict: "malicious" | "review" | "benign";
  severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO";
  confidence: "high" | "medium" | "low";
  maliciousness_score: number;       // 0-100
  triage_priority: string;           // P1, P2, P3, P4
  mitre_techniques: string[];        // e.g. ["T1059.001"]
  signal_count: number;
  noise_count: number;
  analysis_duration_formatted: string;
}

finding.signal

typescript
// data payload
{
  investigation_id: string;
  hostname: string;
  finding_title: string;
  description: string;
  outcome: "signal";
  severity: string;
  mitre_techniques: string[];
  evidence: string[];
}

endpoint.created

typescript
// data payload
{
  endpoint_id: string;
  hostname: string;
  operating_system: string;
  created_at: string;              // ISO-8601
}

scan.completed / scan.drift_detected

typescript
// data payload
{
  scan_id: string;
  endpoint_id: string;
  hostname: string;
  status: "completed" | "drift_detected";
  drift_summary?: string;
}

Retry Policy

Failed deliveries (non-2xx response or timeout) are retried up to 3 times with increasing delays. Your endpoint must respond with a 2xx status code within 10 seconds.

Attempt Delay
1st (initial) Immediate
2nd retry 5 minutes
3rd retry 30 minutes
4th retry (final) 2 hours

After 4 failed attempts the delivery is marked as permanently failed. Use the X-Surge-Delivery-Id header to deduplicate retries on your end.

Signature Verification

Every webhook delivery includes an HMAC-SHA256 signature in the X-Surge-Signature header. You should always verify this signature to ensure the payload is authentic and hasn't been tampered with.

How It Works

  1. SURGE creates a signing payload: {timestamp}.{json_body}
  2. This payload is signed with your webhook secret using HMAC-SHA256
  3. The hex digest is sent as sha256=<hex> in the header
  4. You reconstruct the signing payload using the X-Surge-Timestamp header and raw body
  5. Compare using a constant-time comparison function to prevent timing attacks

Always verify the timestamp is within a 5-minute window to prevent replay attacks.

Python

python
import hmac
import hashlib
from datetime import datetime, timezone, timedelta

def verify_surge_webhook(body: bytes, headers: dict, secret: str) -> bool:
    """Verify a SURGE webhook signature."""
    signature = headers.get("X-Surge-Signature", "")
    timestamp = headers.get("X-Surge-Timestamp", "")

    if not signature.startswith("sha256="):
        return False

    # Check timestamp freshness (prevent replay attacks)
    try:
        ts = datetime.fromisoformat(timestamp)
        if abs((datetime.now(timezone.utc) - ts).total_seconds()) > 300:
            return False
    except ValueError:
        return False

    # Reconstruct the signing payload
    sign_payload = f"{timestamp}.{body.decode('utf-8')}".encode()
    expected = hmac.new(
        secret.encode("utf-8"),
        sign_payload,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(f"sha256={expected}", signature)

Node.js

javascript
const crypto = require('crypto');

function verifySurgeWebhook(rawBody, headers, secret) {
  const signature = headers['x-surge-signature'] || '';
  const timestamp = headers['x-surge-timestamp'] || '';

  if (!signature.startsWith('sha256=')) return false;

  // Check timestamp freshness
  const ts = new Date(timestamp);
  if (Math.abs(Date.now() - ts.getTime()) > 300_000) return false;

  // Reconstruct the signing payload
  const signPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signPayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(`sha256=${expected}`),
    Buffer.from(signature)
  );
}

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySurgeWebhook(req.body, req.headers, process.env.SURGE_WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  console.log('Verified event:', event.event, event.data);
  res.status(200).send('OK');
});

Go

go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"math"
	"net/http"
	"strings"
	"time"
)

func verifySurgeWebhook(body []byte, r *http.Request, secret string) bool {
	signature := r.Header.Get("X-Surge-Signature")
	timestamp := r.Header.Get("X-Surge-Timestamp")

	if !strings.HasPrefix(signature, "sha256=") {
		return false
	}

	// Check timestamp freshness
	ts, err := time.Parse(time.RFC3339, timestamp)
	if err != nil {
		return false
	}
	if math.Abs(time.Since(ts).Seconds()) > 300 {
		return false
	}

	// Reconstruct the signing payload
	signPayload := fmt.Sprintf("%s.%s", timestamp, string(body))
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(signPayload))
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

	return hmac.Equal([]byte(expected), []byte(signature))
}

SIEM Integration

SURGE is designed for seamless SIEM/SOAR integration. Use incremental polling to pull findings into your security data lake, or stream real-time events via webhooks.

Incremental Polling Pattern

Use the created_after or updated_after timestamp filters to poll for new data since your last sync. This avoids fetching duplicate records.

python
import requests
from datetime import datetime, timezone

SURGE_API = "https://app.surge.security/api/v1"
API_KEY = "surge_your_key_here"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Track your last poll timestamp
last_poll = "2025-06-01T00:00:00Z"

# Poll for new findings since last sync
response = requests.post(
    f"{SURGE_API}/findings",
    headers=HEADERS,
    json={
        "created_after": last_poll,
        "per_page": 200,
    },
)
data = response.json()
for finding in data["findings"]:
    # Ingest into your SIEM
    print(f"[{finding['severity']}] {finding['hostname']}: {finding['title']}")

# Update last_poll for next iteration
last_poll = datetime.now(timezone.utc).isoformat()

CEF Output

Request CEF (Common Event Format) output by adding ?format=cef to findings endpoints. CEF is the standard syslog format for Splunk, ArcSight, QRadar, and other SIEMs.

bash
# Get CEF-formatted findings for a specific investigation
curl -s "https://app.surge.security/api/v1/investigations/1234/findings?format=cef" \
  -H "X-API-Key: surge_your_key_here"
text
CEF:0|SURGE|SecurityForensics|1.1.0|finding|PowerShell Encoded Command|7|src=WORKSTATION-01 cs1=1234 cs1Label=InvestigationID cs2=signal cs2Label=Outcome cs3=confirmed cs3Label=Status cs4=T1059.001 T1027 cs4Label=MitreTechniques msg=Encoded PowerShell command detected...

Splunk / SOAR Integration

For Splunk integration, configure a scripted input or HTTP Event Collector (HEC) that polls the SURGE API on a schedule. Use the bulk /findings endpoint with CEF format for efficient ingestion:

  1. Create a SURGE API key with read scope
  2. Configure a polling interval (5-15 minutes recommended)
  3. Use created_after with your last poll timestamp for incremental sync
  4. Pipe CEF output directly to your syslog collector or HEC endpoint

Alternatively, use webhooks with the finding.signal event for real-time alerting — each signal-outcome finding is pushed individually as it's detected.

Errors & Rate Limiting

Error Format

All errors return a JSON object with a detail field describing the issue:

json
{
  "detail": "Scope 'read' required"
}

HTTP Status Codes

Code Meaning
200 Success
400 Bad request — invalid parameters or malformed body
401 Unauthorized — missing or invalid API key
402 Payment required — investigation quota exhausted
403 Forbidden — insufficient scope or plan doesn't include API access
404 Not found — investigation or resource doesn't exist
429 Rate limited — too many requests
500 Internal server error

Rate Limiting

The API enforces rate limits to ensure platform stability:

Limit

300 requests

Window

60 seconds

Per IP address. Exceeding this limit returns 429 Too Many Requests.

Best Practices

  • Use incremental polling with created_after / updated_after to minimize redundant requests
  • Set per_page to 200 (maximum) for bulk operations
  • Prefer webhooks over polling for real-time event processing
  • Implement exponential backoff when receiving 429 responses
  • Cache the OpenAPI spec (/openapi.json) locally rather than fetching it on every request
  • Use the X-Surge-Delivery-Id header to deduplicate webhook deliveries