Documentation beta
How to use the scanner - the web app and the JSON API. Results are informational only, produced by automated tests.
Beta access beta
During the closed beta the scanner is invite-only. You need a personal
access code - it looks like bk-…. No code yet?
Request beta access.
Web app beta
- Magic link (easiest) - open the invite link
https://scan.stalkphish.io/?code=bk-yourcode. Your code is remembered on this browser and the code is stripped from the address bar. Nothing else to do. - Or enter it by hand - on the Scan page, type your code in the “Closed beta” box and press Enter.
- Your code is kept in a secure, http-only cookie, so you won't be re-prompted on this browser until it expires. If a scan later reports invalid access, your code was revoked or expired - just re-enter it.
API beta
Send your code on every request, in either header
(X-Access-Key or a Bearer token):
curl -s -X POST https://scan.stalkphish.io/api/scan \
-H 'Content-Type: application/json' \
-H 'X-Access-Key: bk-yourcode' \
-d '{"url":"https://suspicious-example.com/login"}'
# equivalently:
# -H 'Authorization: Bearer bk-yourcode'| Response | Meaning |
|---|---|
202 | Accepted - a scan was queued. |
401 | Missing, invalid, or revoked access code. |
429 | Your code's quota is spent - see the Retry-After header. |
Each code has its own quota. Please scan responsibly, and use “⚑ Report a false positive or issue” on any wrong verdict - that feedback is exactly what the beta is for.
Using the web app
- Paste a URL on the Scan page and press Scan URL.
Defanged URLs are accepted -
hxxps://evil[.]comis read ashttps://evil.com. If you omit the scheme,https://is assumed. - The scan runs asynchronously. You are taken to a result page that updates live: Queued… → the current phase (Resolving DNS & hosting, Querying threat intelligence, Fetching & analysing the page, Rendering the page…) → the final result.
- Under heavy load a submission may be rejected with a “scanner is busy” message - retry in a moment.
The web app never fetches the target from your browser. The URL is analysed from isolated infrastructure, through proxies - your IP is never used.
Reading a result
The header shows a threat score out of 100 - higher means more evidence of phishing, not more trust - and a verdict:
| Verdict | Score | Meaning |
|---|---|---|
| CRITICAL | 80–100 | Almost certainly phishing. |
| HIGH | 55–79 | Strong phishing indicators. |
| MEDIUM | 35–54 | Suspicious - investigate further. |
| LOW | 18–34 | Weak signals. |
| VERY LOW | 0–17 | No significant indicators. |
The report is organised in sections:
- Screenshot - a snapshot of the page rendered from isolated infrastructure (not a live page).
- Detections - the scored signals that fired (generic labels; the exact detection logic is intentionally not exposed).
- StalkPhish.io Threat intelligence - IP reputation - shown only when the resolved IP is already known for hosting other phishing.
- HTTP redirect chain, Network, HTTP, TLS certificate, Domain / WHOIS, Threat intel - StalkPhish - forensic metadata.
Every hostile URL is shown defanged (hxxp://…[.]…) so it can't be
clicked by accident. Each result also carries the scan time (UTC).
Think a verdict is wrong? Use “⚑ Report a false positive or issue” at the bottom of the result - it attaches the exact scan so we can reproduce and tune.
Limits & privacy
- Rate limit - leave 5 seconds between two submissions, and no more than 1000 per hour per IP. Over either, you get “Too many scans from your network” (HTTP 429). Full detail in Rate limits.
- Ephemeral - results live for a limited time and are shared only via their
unique link (
/results/<uuid>), which search engines don't index. - No warranty - results come from automated tests, are not a definitive verdict, and are provided without warranty. Always verify independently.
API
Everything the web app does is available over a JSON API. The interactive reference is Swagger UI:
beta Authentication required.
Every POST /api/scan needs your access code
(X-Access-Key: bk-…) - see Beta access above.
Open API reference (Swagger) → OpenAPI schema
Asynchronous flow
A scan is a job: submit a URL, receive a uuid, then poll until it's done.
| Method | Path | Purpose |
|---|---|---|
| POST | /api/scan | Submit a URL → 202 {uuid, status:"queued"} |
| GET | /api/scan/{uuid} | Status & result (queued → running → done | error) |
| GET | /api/scan/{uuid}/screenshot | Rendered screenshot (PNG) |
| POST | /api/report | Report a false positive / issue on a scan |
Example
# 1. Submit - returns a UUID immediately
curl -s -X POST https://scan.stalkphish.io/api/scan \
-H 'Content-Type: application/json' \
-H 'X-Access-Key: bk-yourcode' \
-d '{"url":"https://suspicious-example.com/login"}'
# → {"uuid":"a1b2c3d4-...","status":"queued"}
# 2. Poll until status == "done"
curl -s https://scan.stalkphish.io/api/scan/a1b2c3d4-...
# → {"status":"done","result":{"score":92,"verdict":"CRITICAL", ... }}
# 3. (optional) fetch the screenshot
curl -s https://scan.stalkphish.io/api/scan/a1b2c3d4-.../screenshot -o shot.pngThe edge only enqueues - it never fetches the target itself (SSRF containment). Defanged URLs are accepted.
Rate limits
Submitting is paced; reading a result is not. Your client is expected to sleep between submissions - the scanner refuses what arrives too fast rather than queueing it, so a batch sent all at once is mostly rejected, not merely delayed.
| Limit | Value | Applies to |
|---|---|---|
| Minimum gap between submissions | 5 s | POST /api/scan, per IP |
| Submissions per window | 1000 per hour | POST /api/scan, per IP |
| Beta access code quota | 300 per day | POST /api/scan, per code |
| Feedback reports | 10 per hour | POST /api/report, per IP |
| Reading results | not metered | GET /api/scan/… |
How to know where you stand
Every answer to POST /api/scan carries your current standing - on the
202 as much as on the 429. Read them and you never need to
be refused:
| Header | Meaning |
|---|---|
RateLimit-Limit | Submissions allowed in the window. |
RateLimit-Remaining | How many you have left. Stop at 0. |
RateLimit-Reset | Seconds until the window refills. |
RateLimit-Policy | The window itself, e.g. 1000;w=3600. |
Retry-After | On 429 and 503: seconds to wait before retrying. |
Submitting a batch
One URL every 5 seconds. This is the whole recipe:
while read -r url; do
curl -s -X POST https://scan.stalkphish.io/api/scan \
-H 'Content-Type: application/json' \
-H 'X-Access-Key: bk-yourcode' \
-d "{\"url\":\"$url\"}"
echo
sleep 5
done < urls.txtTo watch your budget while you do it, ask curl for the headers:
curl -s -D - -o /dev/null -X POST https://scan.stalkphish.io/api/scan \
-H 'Content-Type: application/json' \
-H 'X-Access-Key: bk-yourcode' \
-d '{"url":"https://suspicious-example.com/login"}' \
| grep -i '^ratelimit-'
# RateLimit-Limit: 1000
# RateLimit-Remaining: 999
# RateLimit-Reset: 3599
# RateLimit-Policy: 1000;w=3600| Status | Meaning | What to do |
|---|---|---|
202 | Queued. | Poll GET /api/scan/{uuid}. |
429 | Too fast, or window spent. | Sleep Retry-After seconds. Do not retry immediately. |
503 | Scanner busy - the queue is full. | Sleep Retry-After seconds. Nothing was charged to your quota. |
A 429 from the edge is JSON
({"detail":"rate limit exceeded","retry_after":5}),
so a script can read it without parsing HTML. Need a higher rate for a legitimate
bulk job? Get in touch rather than working
around the limit.
Results come from automated tests, for information only and without warranty. They may be inaccurate and are not a definitive verdict - StalkPhish.io accepts no liability; always verify independently.