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.
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.
Make your first request
Verify connectivity with the health check endpoint:
curl -s https://app.surge.security/api/v1/ping \
-H "X-API-Key: surge_your_key_here" | jq .{
"status": "ok",
"api_version": "1.1.0"
}Fetch your investigations
List completed investigations with optional filters:
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.
# 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
/api/v1/ping scope: readHealth check and authentication validation. Use this to verify your API key is working.
Response
{
"status": "ok",
"api_version": "1.1.0"
}/api/v1/openapi.json scope: readOpenAPI 3.x specification for the SURGE API. Use this to auto-generate SDK clients, Postman collections, or SIEM/SOAR connectors.
Investigations
/api/v1/investigations scope: readList 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
{
"status": "complete",
"severity": "HIGH",
"created_after": "2025-01-01T00:00:00Z",
"page": 1,
"per_page": 25
}Response
{
"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"]
}
]
}/api/v1/investigations/{investigation_id} scope: readGet full details for a single investigation, including verdict, MITRE techniques with severity scores, and evidence indicators.
Response
{
"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
}
}
}/api/v1/investigations/{investigation_id}/status scope: readLightweight status poll. Use this to check if an investigation is complete without fetching full details.
Response
{
"investigation_id": "1234",
"status": "complete",
"verdict": "malicious",
"confidence": "high",
"boost_status": null
}/api/v1/investigations/{investigation_id}/findings scope: readGet 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
{
"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"]
}
]
}/api/v1/investigations/{investigation_id}/sigma_rules scope: readGet auto-generated Sigma detection rules for an investigation. Rules are derived from the attack techniques observed during analysis.
Response
{
"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)
/api/v1/findings scope: readFlat 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
{
"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
/api/v1/endpoints scope: readList 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
{
"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
/api/v1/collect scope: writeTrigger 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
{
"hostname": "WORKSTATION-01",
"operating_system": "windows",
"effort_level": "triage"
}Response
{
"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.
# 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"]
}'{
"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
{
"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
// 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
// data payload
{
investigation_id: string;
hostname: string;
finding_title: string;
description: string;
outcome: "signal";
severity: string;
mitre_techniques: string[];
evidence: string[];
}endpoint.created
// data payload
{
endpoint_id: string;
hostname: string;
operating_system: string;
created_at: string; // ISO-8601
}scan.completed / scan.drift_detected
// 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
- SURGE creates a signing payload:
{timestamp}.{json_body} - This payload is signed with your webhook secret using HMAC-SHA256
- The hex digest is sent as
sha256=<hex>in the header - You reconstruct the signing payload using the
X-Surge-Timestampheader and raw body - 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
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
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
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.
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.
# 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"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:
- Create a SURGE API key with
readscope - Configure a polling interval (5-15 minutes recommended)
- Use
created_afterwith your last poll timestamp for incremental sync - 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:
{
"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_afterto minimize redundant requests - Set
per_pageto 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-Idheader to deduplicate webhook deliveries
