secscan API overview
The secscan API lets you automate everything the web interface does: submit files, repositories and links, follow the analysis live and fetch the full forensic report as JSON, from scripts, CI pipelines and your own tools.
- REST over HTTPS, JSON in and out, base URL
https://one.heimdall-security.net/api/v1 - Every submission is analysed in the same isolated, network-less sandbox as web uploads
- Large files are sent in resumable chunks of 16 MB
- Available with secscan Pro; create keys in API & CLI
Authentication
Send your API key in the X-Secscan-Token header with every request. Keys start with hsk_, are shown once when created and can be revoked at any time. Treat them like passwords: never put them in public repositories or client-side code.
curl -H "X-Secscan-Token: $SECSCAN_TOKEN" https://one.heimdall-security.net/api/v1/me
Plans & quotas
| Plan | Scans | File size | API |
|---|---|---|---|
| Guest | 3 per week | 10 MB | – |
| Free | 10 per week | 100 MB | – |
| Pro | unlimited | 3.0 GB | ✓ |
Each key may make 120 requests per minute and run 2 scans at the same time.
Errors
Errors use standard HTTP status codes with a JSON body {"error": "...", "message": "..."}.
400 | invalid request or quota reached (see error) |
401 | missing_token or invalid_token (unknown or revoked) |
403 | pro_required: the account has no active Pro plan |
404 | not found (also for other users' scans) |
409 | chunk out of order: continue at the returned received offset |
429 | rate_limited: slow down |
Account
GET /me returns your email, plan, Pro end date, file size limit and chunk size.
Uploads
Uploads are resumable. Start an upload, send the file in chunks, then finish it to start the scan. Any file is accepted: zip, tar, tar.gz/.tgz, tar.bz2, tar.xz, gz, bz2, xz, 7z and RAR archives are unpacked in the sandbox (recognised by content, not by name); any other file is scanned as it is. To scan several loose files, pack them into one archive.
POST /uploads with {"name": "site.zip", "size": 123456} → {"id", "chunk_size", "received"}
PUT /uploads/{id}?offset=N with the raw bytes of the next chunk → {"received"}. After a network error, GET /uploads/{id} tells you where to continue.
POST /uploads/{id}/finish → {"scan_id"}
Repository & URL imports
POST /imports with {"kind": "github", "source": "owner/repo", "ref": "main"} or {"kind": "url", "source": "https://…"} → {"scan_id"}. Downloads happen in a separate container that can only reach public internet addresses.
Website scans (OWASP ZAP)
POST /website-scans with {"url": "https://www.example.com/", "mode": "light"} → {"scan_id"}. light crawls the site and runs passive checks without attacks. full adds the active scan and is only accepted for domains you verified on the Websites page. The report (/scans/{id}/report) contains alerts with description, solution, references, CWE and the affected addresses.
GET /domains lists your domains and whether their ownership is verified.
Platform integration (service API)
For the operator's own platforms (Heimdall-Security.net, Netcore.Digital, Webservice.Digital, US24.net) there are service tokens (hss_…), created by an administrator under Admin › Platform integrations. A service client has scopes (scans, users, export, stats), an optional list of allowed source IPs (for example the VPN range) and an optional webhook. It uses the same header, X-Secscan-Token, and all endpoints above.
- Acting user. Without further headers a service token acts as the client's own service account (Pro). With
X-Secscan-On-Behalf: <external id>(scopeusers) it acts as the linked platform user, with that user's plan and limits, so scans appear in their history. - GET
/service/info: client, platform, scopes and acting user. - PUT
/service/users/{external_id}with{"email", "name", "plan": "free"|"pro", "pro_until": "YYYY-MM-DD"}creates or updates a platform user and links it (an existing account with the same email is linked, not duplicated). GET reads it, DELETE unlinks it (?delete_account=1deletes the account and its scans). - GET
/service/users?after=&limit=and/service/scans?since=&cursor=&limit=(scopeexport) page through all accounts and scans for a migration; continue withnext_after/next_cursoruntil it isnull./service/scans/{id}/reportreturns any full report. - GET
/service/stats(scopestats): totals, queue length and users. GET/healthneeds no token.
Webhooks
When a scan of the service account or of a linked user finishes, secscan sends POST with {"event": "scan.finished", "external_id", "scan": {…}, "report"} to the client's webhook address (only the operator's own domains are accepted). Headers: X-Secscan-Event, X-Secscan-Delivery (unique ID: ignore repeats) and X-Secscan-Signature: t=<unix time>,v1=<hex>, the HMAC-SHA256 of "<t>.<raw body>" with the webhook secret. Answer with any 2xx within 15 seconds; other answers are retried after 1, 5 and 15 minutes, then hourly up to 12 hours. POST /service/webhooks/test sends a ping.
// PHP: verify a webhook before trusting it
[$t, $v1] = sscanf($_SERVER['HTTP_X_SECSCAN_SIGNATURE'], 't=%d,v1=%64s');
$body = file_get_contents('php://input');
$ok = abs(time() - $t) < 300 && hash_equals(hash_hmac('sha256', "$t.$body", $secret), (string) $v1);
Scans & reports
GET /scans lists your scans.
GET /scans/{id} returns the status. While status is queued, progress shows the current stage, counters and new findings (pass ?since= the last seq you saw).
GET /scans/{id}/report returns the full forensic report: verdict, findings with code excerpts, file inventory with hashes, engine versions and timings.
Detection data
GET /data/manifest, /data/osv (vulnerability database, SQLite) and /data/yara (YARA rule sources with their licences) are used by the self-hosted CLI for local scans.
Self-hosted CLI
Pro users can run the secscan scanner on their own machines (Python 3.8+, no dependencies). It needs an API key and verifies it at least every 7 days.
python3 secscan.py login # paste your API key (input hidden) python3 secscan.py update # download the detection data python3 secscan.py all ./project # scan locally python3 secscan.py remote scan ./project # scan on secscan servers and print the report
Examples
Upload a file in one chunk (files up to the chunk size) and wait for the verdict:
ID=$(curl -s -H "X-Secscan-Token: $SECSCAN_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"site.zip","size":'$(stat -c%s site.zip)'}' https://one.heimdall-security.net/api/v1/uploads | jq -r .id)
curl -s -X PUT -H "X-Secscan-Token: $SECSCAN_TOKEN" --data-binary @site.zip "https://one.heimdall-security.net/api/v1/uploads/$ID?offset=0"
SCAN=$(curl -s -X POST -H "X-Secscan-Token: $SECSCAN_TOKEN" https://one.heimdall-security.net/api/v1/uploads/$ID/finish | jq -r .scan_id)
curl -s -H "X-Secscan-Token: $SECSCAN_TOKEN" https://one.heimdall-security.net/api/v1/scans/$SCAN | jq .status,.verdict
Python (standard library):
import json, os, urllib.request
def api(method, path, body=None):
req = urllib.request.Request("https://one.heimdall-security.net/api/v1" + path, method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"X-Secscan-Token": os.environ["SECSCAN_TOKEN"], "Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return json.load(r)
scan = api("POST", "/imports", {"kind": "github", "source": "octocat/Hello-World"})
print(api("GET", f"/scans/{scan['scan_id']}")["status"])