Skip to main content

Platform API — Reference

Base URL: /api/v1. All responses are JSON (Content-Type: application/json) — a JSON object for a single resource, a JSON array for a collection (paginated with limit/offset). Nothing is streamed or JSONL. Times are UTC ISO 8601.

Machine-readable contract. The server publishes a live OpenAPI 3 schema — use it to generate typed clients and validate payloads:

  • Swagger UI: /docs · ReDoc: /redoc · raw spec: /openapi.json

The examples and the "Data shapes & enums" section below mirror that schema.

Authentication (three keys)

Every request sends the master and admin keys as headers. Org-scoped requests additionally send the org key.

HeaderRequired onMeaning
X-Master-Keyevery requestglobal platform secret (server config)
X-Admin-Keyevery requestoperator / UI-backend identity
X-Org-Keyorg-scoped requestsper-org key; selects the org

Rules:

  • A key is never placed in a URL. The org is selected by the X-Org-Key header; admin routes address an org by the caller's own customer_id.
  • Keys are shown once at creation and stored only as salted hashes.
  • Missing a required header → 422. Wrong master → 401 (or 503 if the server has no master key configured). Wrong admin/org key → 401.
  • Rate limit: 300 requests/minute per admin key → 429 when exceeded.

Errors

Standard FastAPI error envelope: {"detail": "<message>"} with the HTTP status code. Common codes: 401 (bad key), 403 (module not enabled), 404 (not found), 409 (duplicate customer_id), 422 (missing/invalid input), 429 (rate limited).


Admin endpoints (master + admin)

These manage admin keys, orgs, and the audit log. Orgs are addressed by your own customer_id, never the platform UUID.

Admin keys (master key only)

MethodPathBodyReturns
POST/admin-keys{"name": "ui-backend"}201 admin key (plaintext, once)
GET/admin-keyslist (prefixes only)
DELETE/admin-keys/{id}204 revoke

Audit (master key only)

MethodPathQueryReturns
GET/auditlimit (1–1000)recent mutating calls; key prefixes only

Orgs

MethodPathBodyReturns
POST/orgs{customer_name, customer_id, domains?, modules?}201 org + org key (once)
GET/orgslist of orgs
GET/orgs/{customer_id}org detail (no key)
POST/orgs/{customer_id}/rotate-keynew org key (once); old key stops working

POST /orgs request:

{
"customer_name": "Acme Corp",
"customer_id": "CUST-100",
"domains": ["acme.com", "www.acme.com"],
"modules": ["easm"]
}

201 response (the only time the key is shown):

{
"id": "05a5e76e-05a3-4dd0-a20c-2f8f5a21352c",
"customer_id": "CUST-100",
"customer_name": "Acme Corp",
"slug": "cust_100",
"modules_enabled": ["easm"],
"is_active": true,
"api_key_prefix": "org_oGn5cmw8",
"created_at": "2026-06-09T08:00:00Z",
"org_api_key": "org_oGn5cmw8UOM8udxKjCLl4ezpODRgIo4xzjLLurWHIKc"
}

Org endpoints (master + admin + org key)

No id in the path — the X-Org-Key header selects the org. Resource ids (domain_id, scan_id, finding_id) are non-secret UUIDs scoped to the org.

Self

MethodPathReturns
GET/orgthe authenticated org (no key)

Domains

MethodPathBodyReturns
GET/org/domainslist of {id, value, kind}
POST/org/domains{"domains": ["a.com","b.com"]}201 full domain list
PATCH/org/domains/{domain_id}{"value": "c.com"}updated domain (value normalized to lowercase)
DELETE/org/domains/{domain_id}204

Scans

MethodPathBodyReturns
POST/org/scans{"module":"easm","scanners":[],"params":{},"force":false}202 scan job (status pending) · 429/409 see below
GET/org/scansscan history (newest first)
GET/org/scans/{scan_id}full job: status, runs, stats
POST/org/scans/{scan_id}/stopcooperative cancel
GET/org/scans/{scan_id}/progresslive progress
GET/org/scans/{scan_id}/deltareconciliation delta
GET/org/scans/{scan_id}/feedback-validationfeedback outcome stats

scanners empty = the full EASM pipeline. module (default "easm") must be in the org's modules_enabled (else 403). params accepts scan options, e.g. {"ua_mode": "browser"}. force (bool, default false) bypasses the scan cooldown (see below); the bypass is audited.

Scan cooldown

To prevent accidental re-scanning, only one scan per (org, module) per interval is allowed (default 14 days). EASM and DAST keep independent clocks — running an EASM scan does not start the DAST cooldown, and vice versa. The interval is configurable per org via the scan_interval_days setting (default 14). Failed and cancelled scans do not count toward the cooldown, and force: true bypasses it (the bypass is recorded in the audit log).

POST /org/scans request:

{ "module": "easm", "scanners": [], "params": {}, "force": false }

202 Accepted — queued (status pending):

{ "id": "07075497-...", "status": "pending", "scope_id": "...", "runs": [], "created_at": "..." }

429 — a scan for this module ran within the interval (still in cooldown):

{ "error": "scan cooldown active", "module": "easm",
"next_scan_allowed_at": "2026-06-24T08:00:00Z" }

409 Conflict — a scan for the same module is already in flight (pending or running). Wait for it to finish (or stop it) before starting another.

GET /org/scans/{scan_id}/progress:

{
"scan_id": "07075497-...",
"status": "running",
"cancel_requested": false,
"stages_total": 11,
"stages_done": 6,
"current": "easm.httpx",
"assets": 1240,
"findings": 14,
"phase": "running"
}

POST /org/scans/{scan_id}/stop:

{ "scan_id": "07075497-...", "status": "running", "cancel_requested": true }

A pending job is cancelled immediately; a running job is flagged and stops at the next stage boundary (the in-flight tool finishes its step), then reports status: "cancelled".

GET /org/scans/{scan_id}/feedback-validation:

{
"scan_id": "07075497-...",
"confirmed_fixed": 8,
"regressed": 2,
"newly_found": 3,
"auto_resolved": 1,
"still_open": 12,
"accepted_risk": 4,
"false_positive": 2,
"status_breakdown": {"new": 9, "in_progress": 3, "resolved": 1, "...": 0}
}

Findings + feedback

MethodPathQuery / BodyReturns
GET/org/findingsseverity, status, finding_class, include_inventory, include_duplicates, limit, offsetfindings (newest first)
GET/org/findings/{finding_id}one finding
GET/org/findings/{finding_id}/historyevent timeline
PATCH/org/findings/{finding_id}{status, note?, actor?, suppress_similar?, suppress_match?}updated finding
GET/org/tasksstatus, priority, limit, offsetremediation tasks
GET/org/activityaction, actor_kind, target_type, outcome, limit, offsetthe org's activity / audit trail (who/what/when/where/outcome)

Default GET /org/findings view hides inventory-class detections and false-positive findings. Each finding carries: title, severity, status, finding_class, description, risk, remediation_steps (4), references, cve_id/cwe_id/cvss_score, severity_reason (provenance), and evidence.

Feedback PATCH /org/findings/{finding_id}:

{ "status": "resolved", "note": "patched to 2.4.58", "actor": "customer", "suppress_similar": false }

Valid status values for customer feedback: in_progress, resolved, accepted_risk, false_positive. With suppress_similar: true on false_positive/accepted_risk, a rule is stored so matching findings stay suppressed on future scans (suppress_match: template_id | cve | category | title).


Data shapes & enums (for the UI)

Allowed enum values the UI should switch on:

EnumValues
severityinfo, low, medium, high, critical
finding statusnew, in_progress, resolved, confirmed_resolved, reopened, accepted_risk, false_positive (legacy: open, triaged)
finding_classinventory, expected_exposure, should_not_be_public, issue
scan statuspending, running, partial, completed, failed, cancelled
progress phaserunning, done, cancelled
task statusopen, in_progress, blocked, done, wont_do
task prioritylow, medium, high, urgent
domain kinddomain, ip_range, asn, keyword

Finding object (full shape)

{
"id": "uuid",
"dedup_key": "string",
"title": "string",
"description": "string | null",
"severity": "info|low|medium|high|critical",
"source_severity": "string | null",
"severity_reason": ["base:medium (source)", "↑ EPSS 0.94"],
"status": "new|in_progress|resolved|confirmed_resolved|reopened|accepted_risk|false_positive",
"finding_class": "inventory|expected_exposure|should_not_be_public|issue",
"category": "string | null",
"risk": "string | null",
"remediation": "string | null",
"remediation_steps": ["step 1", "step 2", "step 3", "step 4"],
"cve_id": "CVE-2021-40438 | null",
"cwe_id": "CWE-200 | null",
"cvss_score": 9.0,
"references": ["https://..."],
"evidence": { "matched_at": "https://...", "port": 443, "...": "tool evidence" },
"asset_id": "uuid | null",
"scope_id": "uuid | null",
"first_seen": "2026-06-09T08:00:00Z",
"last_seen": "2026-06-09T08:00:00Z"
}

Scan job object (GET /org/scans/{scan_id})

{
"id": "uuid",
"scope_id": "uuid",
"status": "pending|running|partial|completed|failed|cancelled",
"requested_scanners": [],
"params": { "module": "easm" },
"stats": {
"assets": 1240, "findings": 14,
"progress": { "stages_total": 11, "stages_done": 11, "current": null,
"assets": 1240, "findings": 14, "phase": "done" },
"finalize": { "delta": { "new": 9, "reopened": 2, "confirmed_resolved": 8,
"still_open": 12, "auto_resolved": 1 } }
},
"error": null,
"started_at": "2026-06-09T08:00:00Z",
"finished_at": "2026-06-09T08:12:00Z",
"created_at": "2026-06-09T08:00:00Z",
"runs": [
{ "scanner_key": "easm.subfinder", "scanner_type": "oss_wrapper",
"status": "completed", "raw_count": 2523, "asset_count": 2523,
"finding_count": 0, "error": null,
"started_at": "2026-06-09T08:00:05Z", "finished_at": "2026-06-09T08:02:10Z" }
]
}

Scan delta (GET /org/scans/{scan_id}/delta)

{ "new": 9, "reopened": 2, "confirmed_resolved": 8, "still_open": 12, "auto_resolved": 1 }

Domains list (GET /org/domains)

[ { "id": "uuid", "value": "acme.com", "kind": "domain" },
{ "id": "uuid", "value": "www.acme.com", "kind": "domain" } ]

Task object (GET /org/tasks)

[ { "id": "uuid", "title": "Remediate: End-of-Life Apache HTTP Server",
"description": "string | null", "status": "open",
"priority": "low|medium|high|urgent", "assignee": null, "due_date": null,
"finding_id": "uuid | null", "created_at": "...", "updated_at": "..." } ]

Finding history (GET /org/findings/{id}/history)

[ { "id": "uuid", "event": "triaged", "old_status": "new",
"new_status": "resolved", "actor": "customer", "note": "patched",
"created_at": "2026-06-09T08:30:00Z" } ]

Admin key list (GET /admin-keys) and audit (GET /audit)

// GET /admin-keys (prefixes only)
[ { "id": "uuid", "name": "ui-backend", "key_prefix": "adm_AkfdbuOu",
"active": true, "created_at": "..." } ]

// GET /audit (key prefixes only, never full keys)
[ { "id": "uuid", "method": "POST", "path": "/api/v1/org/scans",
"status_code": 202, "admin_prefix": "adm_AkfdbuOu",
"org_prefix": "org_oGn5cmw8", "ip": "203.0.113.5", "created_at": "..." } ]

Error envelope (any non-2xx)

{ "detail": "Invalid org key" }