Skip to main content

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-Key and X-Admin-Key. Calls that act on a specific org also send X-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

KeyHeld byLifetime
Master keythe platform deployment (server config)long-lived; rotate rarely
Admin keyyour UI backendper integration; revocable
Org keyyour UI backend, one per customer orgper 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:

FieldMeaning
confirmed_fixedcustomer said "resolved" and the rescan no longer sees it
regressedwas resolved but reappeared (the fix did not hold)
newly_foundfindings new since the previous scan
auto_resolvedopen findings gone for two consecutive scans
still_opennew + in-progress + regressed
accepted_risk / false_positivesuppressed 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-key issues 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) per scan_interval_days (default 14). 429 {"error":"scan cooldown active", ...} while in cooldown, 409 if a scan for that module is already in flight; force:true bypasses (audited). Failed/cancelled scans don't count.
  • Errors: {"detail": "..."}; 401 bad key, 403 module not enabled, 404 not found, 409 duplicate customer_id or scan-in-flight, 422 missing header/field, 429 rate limited or scan cooldown.