Umbra
← All docs

Partner API (v1)

Bearer-token authenticated read API for B2B partners + integrators. Pull findings, targets, host inventory, and deep audit reports as JSON.

Last updated Jun 21, 2026

What it is

A read-only HTTP API at /api/v1/... for partners, integrators, or your own backend tooling to pull the workspace’s findings + target inventory as JSON. Authenticates via a per-org bearer token minted by an org admin.

Use cases:

  • A SaaS vendor scans on Umbra; their partner builds a frontend on top of the data and pulls findings via the API.
  • A SOC team has its own ticketing system and wants Umbra’s confirmed findings as a feed.
  • A compliance team’s GRC tool needs the raw findings data for recurring reports.

Authentication

Every request requires Authorization: Bearer umbra_live_<...>. Keys are minted in API keys under the ADMIN section of the sidebar (org admin role required).

  • 256 bits of entropy per key.
  • bcrypt at rest. The plaintext is shown ONCE at create time. Store it in a password manager or secret store; there is no way to recover it.
  • Optional expiry at create time (30 / 90 / 180 / 365 days, or never).
  • Revocable; a revoked key returns 401 on subsequent calls.
  • Usage telemetry (last-used timestamp + IP + total use count) per key on the management page so you can tell which keys are live.
  • Every create + revoke writes to the audit log.

The key body is prefixed umbra_live_ so GitGuardian / TruffleHog and similar secret-scanners catch leaks via regex.

Endpoints

All endpoints are GET-only at v1. Standard error shape: 400 on bad input, 401 on missing/invalid key, 404 on not-found, 500 on server error. Pagination uses page + page_size query params; results are wrapped in {items, total, page, page_size}.

GET /api/v1/targets

List the workspace’s targets.

Query params:

  • q: substring match on target value / note
  • page, page_size: pagination (page_size cap: 500, default 50)

Response shape:

{
  "items": [
    {
      "id": 123,
      "value": "convert.com",
      "kind": "hostname",
      "note": "",
      "paused": false,
      "last_scanned_at": "2026-06-02T15:30:00Z",
      "next_scan_at":   "2026-06-03T15:30:00Z",
      "created_at":     "2026-04-15T10:00:00Z",
      "company": "Convert",
      "business_owner":      "[email protected]",
      "criticality":         "prod",
      "data_classification": "confidential",
      "vendor":              "",
      "host_count":          18,
      "finding_count":       7
    }
  ],
  "total": 42, "page": 1, "page_size": 50
}

GET /api/v1/targets/{id}

Single target detail. Same fields as the list row plus scan_interval_seconds and a hosts[] array (up to 500 entries) with ip, hostname, os, country, country_code, asn, as_org, services_up, first_seen_at, last_seen_at.

GET /api/v1/targets/{id}/findings

The headline endpoint. ALL findings for a target with every field a frontend could want.

Query params:

  • severity=critical,high,medium,low,info: comma-separated
  • status=open,in_progress,fixed,verified,wontfix: comma-separated
  • include_rejected=true: include triage='rejected' rows (default: off)
  • page, page_size: pagination (default 100, cap 500)

Response shape (one item):

{
  "id": 9201,
  "severity": "critical",
  "kind": "rce",
  "title": "Authenticated RCE via export endpoint",
  "url": "https://api.convert.com:443/exports/run?format=…",
  "impact": "Remote code execution on the API node…",
  "recommendation": "Validate the `format` query parameter…",
  "evidence": "POST /exports/run … 200 OK, command output in body…",
  "reproduce": "curl -H 'Authorization: …' …",
  "confidence": 0.92,
  "verdict": "compromised",
  "confirmed": true,
  "poc": {
    "request":  "POST /exports/run HTTP/1.1\nHost: api.convert.com\n…",
    "response": "HTTP/1.1 200 OK\nContent-Type: text/plain\n…",
    "steps": [
      { "label": "Authenticate", "request": "…", "response": "…" },
      { "label": "Trigger",      "request": "…", "response": "…" }
    ]
  },
  "status":             "in_progress",
  "assignee_email":     "[email protected]",
  "due_at":             "2026-06-10T17:00:00Z",
  "closed_at":          null,
  "status_changed_at":  "2026-06-02T09:14:00Z",
  "created_at":         "2026-06-01T22:03:00Z",
  "service": {
    "id":       4421,
    "ip":       "203.0.113.42",
    "hostname": "api.convert.com",
    "port":     443,
    "product":  "node.js",
    "version":  "20.11.1"
  },
  "target": {
    "id":                  123,
    "value":               "convert.com",
    "kind":                "hostname",
    "company":             "Convert",
    "business_owner":      "[email protected]",
    "criticality":         "prod",
    "data_classification": "confidential",
    "vendor":              ""
  },
  "run": {
    "id":        7741,
    "technique": "web_auth_bypass",
    "verdict":   "compromised"
  }
}

GET /api/v1/findings

Org-wide finding list. Same response shape as /targets/{id}/findings. Supports a target_id query param for clients that prefer filtering by query string.

GET /api/v1/findings/{id}

Single finding by id, scoped to the calling org’s targets.

GET /api/v1/deep-audits/{id}

One deep audit report: the full output of an AI-driven deep audit scan, scoped to the calling org. This is the partner-safe projection of the in-app report: the complete security findings, risk rollup, host list, and credential labels, with all cost, billing, and internal-operations fields stripped.

The response is a single object (not a paginated list):

{
  "id": 1230,
  "target_domain": "php.testsparker.com",
  "target_url": "http://php.testsparker.com/",
  "status": "done",
  "phase": null,
  "progress_pct": 100,
  "destructive_ops_allowed": true,
  "failure_reason": null,
  "partial_coverage": false,
  "started_at":  "2026-06-09T16:27:33Z",
  "finished_at": "2026-06-09T17:41:02Z",
  "created_at":  "2026-06-09T16:27:33Z",
  "summary": {
    "risk_score": 100,
    "risk_band": "critical",
    "findings_total": 43,
    "triager_rejected": 74,
    "ai_explorer_found": 3,
    "findings_by_severity": {
      "critical": 4, "high": 13, "medium": 8, "low": 13, "info": 5
    },
    "findings_by_source": {
      "jupitersec_validated": 40, "ai_explorer_tier3": 3
    }
  },
  "hosts": [
    {
      "hostname": "php.testsparker.com",
      "target_url": "http://php.testsparker.com/",
      "is_primary": true,
      "role": "",
      "detected_via": "user_typed",
      "confidence": 0
    }
  ],
  "credentials": [
    {
      "label": "app login",
      "kind": "form_login",
      "notes": null,
      "created_at": "2026-06-09T16:27:33Z",
      "revoked_at": null
    }
  ],
  "findings": [
    {
      "id": 11258,
      "deep_scan_id": 1230,
      "severity": "critical",
      "kind": "command_injection",
      "title": "Remote Code Execution via Command Injection in nslookup.php",
      "url": "http://php.testsparker.com/nslookup.php",
      "source": "ai_explorer_tier3",
      "confidence": 0.95,
      "confirmed": true,
      "evidence": "The `$param` value is interpolated into a shell command with no escaping…",
      "impact": "Unauthenticated attackers can execute arbitrary commands on the host…",
      "reproduce": "curl -s -X POST 'http://php.testsparker.com/nslookup.php' …",
      "detail": "Do not pass user input to shell_exec. Use escapeshellarg()…",
      "business_impact": "Full server compromise and lateral movement.",
      "affected_users": "",
      "data_exposed": "",
      "cwe": ["CWE-78"],
      "owasp_top10": ["A03:2021 - Injection"],
      "discovered_by": "ai_explorer",
      "detection_method": "active_exploitation",
      "http_method": "POST",
      "affected_parameter": "param",
      "parameter_location": "body",
      "cvss_score": 9.8,
      "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "attack_vector": "network",
      "attack_complexity": "low",
      "privileges_required": "none",
      "user_interaction": "none",
      "scope": "unchanged",
      "remediation_effort": "low",
      "remediation_priority": "immediate",
      "pocs": [
        { "label": "Probe 5: POST /nslookup.php", "request": "POST /nslookup.php HTTP/1.1\n…", "response": "HTTP/1.1 200 OK\n…" }
      ],
      "reproduction_steps": ["Send the POST request above", "Observe whoami output in the <pre> block"],
      "references_json": [
        { "Title": "OWASP: Command Injection", "URL": "https://owasp.org/www-community/attacks/Command_Injection" }
      ],
      "affected_instances": [],
      "triage": "accepted",
      "status": "open",
      "created_at": "2026-06-09T17:10:44Z"
    }
  ]
}

Notes:

  • summary is the report header rollup: a 0–100 risk_score with a risk_band label, finding counts broken down by severity and by source, plus how many the AI triager rejected as false positives.
  • findings[] carries the full rich report per finding: CWE / OWASP classification, CVSS 3.1 metrics, business impact, remediation guidance, and the raw PoC HTTP exchange chain in pocs[]. Per-finding fields are omitted when empty, so a given finding only carries the subset that’s populated.
  • credentials[] lists the authenticated surfaces the audit tested (labels and kinds only). The plaintext secrets are never returned.
  • Cost and internal-ops fields are not exposed. AI spend, billing charges, per-host surcharges, and worker/debug internals that appear on the in-app report are deliberately stripped from this endpoint.

A missing id and a deep audit that belongs to another org both return 404. The endpoint never confirms whether a scan exists outside your workspace.

Real response example

A live finding returned by the API on a workspace with confirmed exploitable MongoDB exposure. Some long strings are truncated for the docs. The live response carries the full evidence + raw PoC bytes.

{
  "id": 613,
  "severity": "critical",
  "kind": "Broken Authentication / Missing Authentication",
  "title": "Unauthenticated MongoDB Access: Full Read Access to All Databases",
  "url": "mongodb://127.0.0.1:27017",
  "impact": "An attacker with network access to 127.0.0.1:27017 can read, enumerate, and (depending on OS-level mongo permissions) write or drop all data across every database, including the 'admin' database, without supplying any username or password…",
  "evidence": "MongoDB 2.6.10 listens on 127.0.0.1:27017 with no authentication required. Exchange 5 shows a successful unauthenticated `listDatabases` command returning databases ['local', 'admin']. Exchange 8 shows a successful unauthenticated OP_QUERY read against `local.startup_log`…",
  "recommendation": "1. Enable MongoDB authentication immediately: set `security.authorization: enabled` in /etc/mongodb.conf and create strong admin credentials. 2. Upgrade MongoDB from the end-of-life 2.6.10 to a current supported release (7.x). 3. Bind MongoDB to loopback (127.0.0.1)…",
  "confidence": 0.97,
  "verdict": "critical",
  "confirmed": false,
  "poc": {
    "request":  "TCP send → 127.0.0.1:27017 (via agent #29)\n4300000001000000…",
    "response": "TCP recv (192 bytes, 0ms)\nc000000022000000…",
    "steps": [
      { "label": "Step 1: isMaster (unauthenticated handshake confirming open access)", "request": "…", "response": "…" },
      { "label": "Step 2: listDatabases (unauthenticated, returns 'local' and 'admin')", "request": "…", "response": "…" },
      { "label": "Step 3: OP_QUERY local.startup_log (unauthenticated read returning hostname, OS, config paths, version)", "request": "…", "response": "…" }
    ]
  },
  "status": "open",
  "assignee_email": null,
  "due_at": null,
  "closed_at": null,
  "status_changed_at": null,
  "created_at": "2026-05-30T22:20:07Z",
  "service": {
    "id": 6959, "ip": "127.0.0.1", "hostname": "",
    "port": 27017, "product": "Mongodb", "version": null
  },
  "target": {
    "id": 6390, "value": "127.0.0.1/32", "kind": "internal_cidr",
    "company": "", "business_owner": "",
    "criticality": "unspecified", "data_classification": "unspecified",
    "vendor": ""
  },
  "run": { "id": 569, "technique": "mongo_unauth_probe", "verdict": "compromised" }
}

Note the multi-step PoC: each steps[] entry is a discrete protocol exchange the AI engine ran to validate the finding. A frontend can render the chain as a timeline, expand each step on click, or fold the raw bytes into a “show technical details” disclosure.

Examples

Pull every confirmed-exploitable finding for one target

KEY=umbra_live_xxxxxxxx
TARGET=123

curl -s -H "Authorization: Bearer $KEY" \
  "https://app.umbrascope.com/api/v1/targets/$TARGET/findings?severity=critical,high" \
  | jq '.items[] | select(.confirmed == true) | {title, url, severity, recommendation}'

Mirror findings into a downstream system

Standard pagination loop, page until items.length === 0:

for page in $(seq 1 100); do
  res=$(curl -s -H "Authorization: Bearer $KEY" \
    "https://app.umbrascope.com/api/v1/findings?page=$page&page_size=200")
  echo "$res" | jq -r '.items[] | @json' | while read line; do
    ingest "$line"
  done
  [ $(echo "$res" | jq '.items | length') -lt 200 ] && break
done

Pull a deep audit report and list its critical findings

KEY=umbra_live_xxxxxxxx
SCAN=1230

curl -s -H "Authorization: Bearer $KEY" \
  "https://app.umbrascope.com/api/v1/deep-audits/$SCAN" \
  | jq '{risk: .summary.risk_score, band: .summary.risk_band,
         criticals: [.findings[] | select(.severity == "critical")
                     | {title, url, cvss_score}]}'

Why it matters

A B2B partner wanting to build a frontend on top of your scans doesn’t need a screenshot of the dashboard. They need the structured data behind it. Without this they’re either scraping the dashboard or asking you for one-off JSON exports.

Why it’s valuable

  • Self-serve key minting. Org admin creates a key in under a minute; no Umbra support needed.
  • Plaintext shown once. Lost keys get regenerated, not recovered. The only place the plaintext exists is in the recipient’s secret store.
  • Per-key telemetry. Last-used timestamp + IP + use count tells you whether the partner’s actually using the key.
  • Scoped to one org. A key minted in workspace A can’t read workspace B’s data.
  • Revocable. One click; subsequent calls 401 immediately.

Stability

/api/v1/... is a stable contract. We add fields (additive); we don’t rename or remove. Breaking changes ship as /api/v2/....

Roadmap

  • POST /api/v1/targets: create a target from the API (partners often want to start a scan from their own flow).
  • POST /api/v1/scans: trigger a discovery scan.
  • POST /api/v1/exploit/runs: trigger an AI exploit run.
  • Webhooks for new-finding push (so a partner doesn’t have to poll).