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 / notepage,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": "security@convert.com",
"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-separatedstatus=open,in_progress,fixed,verified,wontfix: comma-separatedinclude_rejected=true: includetriage='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": "alice@convert.com",
"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": "security@convert.com",
"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:
summaryis the report header rollup: a 0–100risk_scorewith arisk_bandlabel, 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 inpocs[]. 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).