Platform API — Usage Guide
A practical, end-to-end walkthrough of integrating the platform's scanning API
from your UI (EASM shown; DAST and Cloud follow the same scan lifecycle):
bootstrap keys, create an org, add domains, run a scan, watch progress, read
findings, submit customer feedback, rescan, and read the validation. Every step
shows a real curl and the JSON you get back.
Three keys. Every call sends
X-Master-KeyandX-Admin-Key. Calls that act on a specific org also sendX-Org-Key. Keys are never in URLs. See the API Reference for the full contract.
Throughout, $BASE is your deployment, e.g. https://scanners.example/api/v1.
Tip: the server also publishes a live OpenAPI schema at
/docs(Swagger),/redoc, and/openapi.json— point your client generator at it for typed models of every request and response. The "Data shapes & enums" section of the API Reference lists all field types and enum values.
All responses are JSON (application/json): a JSON object for one resource, a
JSON array for a list. Nothing is JSONL or streamed.
0. Who holds which key
| Key | Held by | Lifetime |
|---|---|---|
| Master key | the platform deployment (server config) | long-lived; rotate rarely |
| Admin key | your UI backend | per integration; revocable |
| Org key | your UI backend, one per customer org | per org; rotatable |
Your UI backend stores, per customer, only: your own customer_id, and the org
key (treat it like a password). You never need the platform's internal UUID.
1. Bootstrap: mint an admin key (once)
The operator holding the master key mints an admin key for the UI backend.
curl -X POST "$BASE/admin-keys" \
-H "X-Master-Key: $MASTER" \
-H "Content-Type: application/json" \
-d '{"name": "ui-backend"}'
{ "id": "...", "name": "ui-backend", "key_prefix": "adm_AkfdbuOu",
"active": true, "created_at": "...", "admin_key": "adm_AkfdbuOu..." }
Store admin_key securely; it is shown only once. Use it as X-Admin-Key from
now on.
2. Create an org (and its first domains)
curl -X POST "$BASE/orgs" \
-H "X-Master-Key: $MASTER" -H "X-Admin-Key: $ADMIN" \
-H "Content-Type: application/json" \
-d '{"customer_name":"Acme Corp","customer_id":"CUST-100",
"domains":["acme.com","www.acme.com"],"modules":["easm"]}'
{ "id":"05a5e76e-...", "customer_id":"CUST-100", "slug":"cust_100",
"modules_enabled":["easm"], "api_key_prefix":"org_oGn5cmw8",
"org_api_key":"org_oGn5cmw8UOM8udxKjCLl4ezpODRgIo4xzjLLurWHIKc", "created_at":"..." }
Store org_api_key against your CUST-100. This is the only time it is shown.
The org's schema is provisioned and the domains are seeded immediately.
Add more domains later, change one, or remove one:
curl -X POST "$BASE/org/domains" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG" \
-H "Content-Type: application/json" -d '{"domains":["shop.acme.com"]}'
curl -X PATCH "$BASE/org/domains/$DOMAIN_ID" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG" \
-H "Content-Type: application/json" -d '{"value":"store.acme.com"}'
curl -X DELETE "$BASE/org/domains/$DOMAIN_ID" \
-H "X-Master-Key: $MASTER" -H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG"
3. Start a scan
curl -X POST "$BASE/org/scans" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG" \
-H "Content-Type: application/json" -d '{"module":"easm"}'
{ "id":"07075497-...", "status":"pending", "scope_id":"...", "runs":[], "created_at":"..." }
Keep the id (the scan_id). The scan runs asynchronously on a worker.
Options in params: e.g. {"module":"easm","params":{"ua_mode":"browser"}} to
blend in as a browser instead of the default branded scanner user-agent.
Scan cooldown (one scan per module per 14 days)
You can run only one scan per (org, module) per interval — default 14
days. EASM and DAST have independent clocks, so starting an EASM scan does
not affect when you can next run DAST. The interval is configurable per org via
the scan_interval_days setting (default 14). Failed and cancelled scans do
not count, so a scan that errors out won't lock you out.
If you start a scan while the module is still in cooldown you get a 429 telling
you when the next one is allowed:
{ "error":"scan cooldown active", "module":"easm",
"next_scan_allowed_at":"2026-06-24T08:00:00Z" }
If a scan for the same module is already running (or queued) you get a 409
instead — wait for it to finish, or stop it (step 5), before starting another.
To force a scan despite the cooldown, send force: true (the override is recorded
in the audit log):
curl -X POST "$BASE/org/scans" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG" \
-H "Content-Type: application/json" -d '{"module":"easm","force":true}'
4. Show progress while it runs
Poll the lightweight progress endpoint (every few seconds):
curl "$BASE/org/scans/$SCAN/progress" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG"
{ "scan_id":"07075497-...", "status":"running", "cancel_requested":false,
"stages_total":11, "stages_done":6, "current":"easm.httpx",
"assets":1240, "findings":14, "phase":"running" }
Render a bar from stages_done / stages_total, label it with current, and show
live assets / findings. status moves pending → running → completed
(or partial / failed / cancelled). phase ends at done or cancelled.
For the full job (per-scanner runs, timings, stats) use GET /org/scans/{scan_id}.
5. Stop a scan
curl -X POST "$BASE/org/scans/$SCAN/stop" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG"
A pending scan cancels immediately. A running scan is flagged and stops at the
next stage boundary (the in-flight tool finishes its current step), then reports
status: "cancelled". Poll /progress until status is cancelled.
6. Read the findings
curl "$BASE/org/findings?severity=high&limit=50" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG"
Each finding includes a plain description, the risk, four remediation steps, and evidence:
{
"id":"...", "title":"End-of-Life Apache HTTP Server", "severity":"high",
"status":"new", "finding_class":"issue",
"description":"CVE-2021-40438: A crafted request uri-path can cause mod_proxy ...",
"risk":"A threat actor can ...",
"remediation_steps":[
"Upgrade the affected component to a fixed release (version 2.4.48 or later).",
"This vulnerability is in the CISA Known Exploited Vulnerabilities catalogue ...",
"Until patched, restrict access and add a web application firewall rule ...",
"Rescan to confirm the fix and monitor for exploitation attempts."],
"cve_id":"CVE-2021-40438", "cvss_score":9.0,
"severity_reason":["base:medium (source)","↑ EPSS 0.94"],
"evidence":{"matched_at":"https://...","port":443}
}
Filters: severity, status, finding_class, include_inventory (default
false), include_duplicates (default false), limit, offset. The default view
shows real signal only (no inventory, no false positives). Use
GET /org/findings/{id} for one finding and /history for its timeline.
GET /org/tasks lists the derived remediation tasks.
7. Submit customer feedback
When the customer answers a finding, PATCH it. Valid feedback statuses:
in_progress, resolved, accepted_risk, false_positive.
curl -X PATCH "$BASE/org/findings/$FINDING" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG" \
-H "Content-Type: application/json" \
-d '{"status":"resolved","note":"patched to 2.4.58","actor":"customer"}'
To suppress a whole class going forward (for false positives or accepted risk):
-d '{"status":"false_positive","suppress_similar":true,"suppress_match":"template_id"}'
A false positive is hidden from the default findings view and a rule keeps matching findings suppressed on future scans. Every change is recorded in the finding's history.
8. Rescan and validate the feedback
After the customer has answered, run another scan (same call as step 3). On a full rescan the engine reconciles each finding against the customer's answer and records the outcome.
curl "$BASE/org/scans/$SCAN/feedback-validation" -H "X-Master-Key: $MASTER" \
-H "X-Admin-Key: $ADMIN" -H "X-Org-Key: $ORG"
{ "scan_id":"...", "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} }
Reading it:
| Field | Meaning |
|---|---|
confirmed_fixed | customer said "resolved" and the rescan no longer sees it |
regressed | was resolved but reappeared (the fix did not hold) |
newly_found | findings new since the previous scan |
auto_resolved | open findings gone for two consecutive scans |
still_open | new + in-progress + regressed |
accepted_risk / false_positive | suppressed counts |
GET /org/scans/{scan_id}/delta gives the raw reconciliation counts
(new / reopened / confirmed_resolved / still_open / auto_resolved).
9. The whole loop (pseudocode)
admin_key = POST /admin-keys # once
org = POST /orgs {customer_id, domains} # store org.org_api_key
scan = POST /org/scans # start
while scan.status in (pending, running): # show progress
p = GET /org/scans/{scan}/progress
render(p.stages_done / p.stages_total, p.current)
findings = GET /org/findings # present results
for answer in customer_answers: # collect feedback
PATCH /org/findings/{id} {status: answer}
rescan = POST /org/scans # validate
v = GET /org/scans/{rescan}/feedback-validation
render(v.confirmed_fixed, v.regressed, v.still_open)
10. Operational notes
- Rate limit: 300 requests/minute per admin key →
429. Poll progress at a few-second cadence, not tighter. - Key rotation:
POST /orgs/{customer_id}/rotate-keyissues a new org key and immediately invalidates the old one. - Audit:
GET /audit(master key) lists recent mutating calls with key prefixes only. - Idempotency / safety: a scan job runs exactly once; a redelivered or retried
job is a no-op. Per-scanner failures are isolated (the job ends
partial, not failed). - Scan cooldown: one scan per
(org, module)perscan_interval_days(default 14).429 {"error":"scan cooldown active", ...}while in cooldown,409if a scan for that module is already in flight;force:truebypasses (audited). Failed/cancelled scans don't count. - Errors:
{"detail": "..."};401bad key,403module not enabled,404not found,409duplicatecustomer_idor scan-in-flight,422missing header/field,429rate limited or scan cooldown.