API — every endpoint is a curl endpoint
Your API key is your login — send it as a Bearer token. Datacenter scanning is free for the first 5 MB (then 1 cr/MB) and the full API is free; residential/mobile egress draws credits. Base URL: https://whack.sh.
Authentication
The key is the credential — there is no separate login step. Send Authorization: Bearer $WHACK_KEY on every request. Only a hash of the key is stored server-side. (In the web app the same key sets an httpOnly session.)
Submit a scan — POST /api/scan
curl -X POST https://whack.sh/api/scan \ -H "Authorization: Bearer $WHACK_KEY" \ -d url=https://example.com/suspicious \ -d egress=datacenter,residential,mobile \ -d cap_blocks=1
→ {"ok":true,"scan_id":123,"status":"queued","egress":["datacenter","residential","mobile"],"max_credits":15}
Submitting the same url + same egress set again within a short window returns the original scan instead of a duplicate: {"ok":true,"scan_id":123,"status":"queued","egress":["datacenter"],"deduped":true}. Treat deduped:true as "already queued".
| param | default | notes |
|---|---|---|
url | — | required · http(s) · ≤ 2048 chars |
egress | datacenter | comma list of datacenter,residential,mobile,byo — run in parallel for the Split-Horizon Diff |
cap_blocks | 1 | bandwidth cap per paid leg: 1/2/3/5/10/20 = up to 5/10/15/25/50/100 MB — the most a leg can bill |
geo | us | 2-letter country. Non-US on the datacenter leg routes it in-country (datacenter · geo, priced like US datacenter: first 5 MB free, then 1 cr/MB); non-US on a residential/mobile leg adds +2 cr flat. US (or blank) keeps the datacenter leg direct |
profile | random | desktop persona for the datacenter / residential capture |
mobile_os | android | android · ios · both (both = two mobile captures) |
BYO is gated to Team Hunter & above (byo_not_entitled) and needs your agent online (byo_offline); BYO submits return status:"pending_accept" until your device picks the job up.
Typical workflow
# 1. Submit the scan curl -X POST https://whack.sh/api/scan \ -H "Authorization: Bearer $WHACK_KEY" \ -d url=https://example.com/suspicious \ -d egress=datacenter,residential # 2. Poll until status is terminal (done / partial / failed / scan_error / expired / cancelled) curl https://whack.sh/api/scan/$ID \ -H "Authorization: Bearer $WHACK_KEY" # 3. Download the full bundle (HARs + screenshots + report + IOCs) curl -OJ https://whack.sh/api/scan/$ID/bundle \ -H "Authorization: Bearer $WHACK_KEY"
Egress & credits
Paid egress is metered: a base credit covers the first 5 MB of a leg, then a small per-MB overage. Datacenter is free for the first 5 MB, then 1 cr/MB. The cap you choose bounds the most a leg can bill, and an offline or unreachable scan is never charged — a leg that pulls essentially nothing costs nothing.
| egress | base (incl. 5 MB) | overage |
|---|---|---|
| datacenter | 0 — free | +1 cr / MB |
datacenter · geo (non-US geo) | 0 — free | +1 cr / MB |
| residential | 5 cr | +1 cr / MB |
| mobile | 10 cr | +2 cr / MB |
| byo (your device) | 2 cr / scan | — (no egress bytes) |
Two ways geo is priced: a non-US country on the datacenter leg routes it in-country, priced exactly like a US datacenter leg (first 5 MB free, then 1 cr/MB); a non-US country on a residential/mobile leg adds a flat +2 cr. US (or blank) keeps the datacenter leg direct. Every datacenter leg is free for the first 5 MB regardless. The report shows the true exit country.
Poll status & list artifacts — GET /api/scan/{id}
curl https://whack.sh/api/scan/123 \ -H "Authorization: Bearer $WHACK_KEY"
Full response for a completed free datacenter scan of https://example.com/:
{
"ok": true,
"scan": {
"id": 178225,
"api_key_id": 22,
"url": "https://example.com/",
"egress_set": ["datacenter"],
"locations": "[]",
"cap_blocks": 1,
"status": "done",
"credits_charged": 0,
"max_credits": 0,
"cloaking_score": 0,
"verdict": "clean",
"verdict_detail": null,
"created_at": 1785630074,
"finished_at": 1785630082,
"error": null,
"share_token": null,
"report_token": "fbbcbe5c4e48a6de6d0c52f6cfc46c0e",
"share_enabled": 0,
"share_detail": "curated",
"flagged": 0,
"jobs": [
{"id":273611,"egress":"datacenter","location":null,"status":"done","gb_blocks_used":1,"bytes":471,"created_at":1785630074,"updated_at":1785630081}
],
"artifacts": [
{"id":465068,"kind":"har","egress":"datacenter","label":"har (datacenter)","size":2593,"created_at":1785630081},
{"id":465069,"kind":"screenshot","egress":"datacenter","label":"screenshot (datacenter)","size":20376,"created_at":1785630081}
]
}
}
| field | meaning |
|---|---|
id | numeric scan id (use it in the path) |
url | the scanned URL |
egress_set | JSON array of the egress legs that ran |
cap_blocks | the chosen byte-cap (1/2/3/5/10/20 blocks = 5/10/15/25/50/100 MB) |
status | lifecycle (see status list below) |
verdict | one of clean, suspicious, malware, phishing, cloaking, unreachable (null until finished) |
cloaking_score | 0-100 divergence score across the egress legs (higher = more cloaking); null until finished |
credits_charged | actual credits billed (0 for a free datacenter scan) |
max_credits | the pre-charge ceiling |
created_at / finished_at | unix epoch seconds (UTC); finished_at null until terminal |
error | a short error string when status is failed/scan_error, else null |
report_token | opens the human report at /app/report/{report_token} in the dashboard (session login) |
share_token / share_enabled / share_detail | set when the owner turns on a public /share/{token} link from the dashboard |
jobs[] | one row PER egress leg → {id, egress, location, status, gb_blocks_used, bytes, created_at, updated_at}. bytes = bytes captured on that leg; gb_blocks_used = billed blocks |
artifacts[] | downloadable files → {id, kind, egress, label, size, created_at}. kind is "har" or "screenshot". Use the id as {aid} in the artifact endpoint. Captured malware bodies are retained server-side and are NEVER listed or served |
Scan status
Lifecycle: queued → (pending_accept, only for byo) → claimed → running → done. Terminal states: done (all legs finished), partial (some legs finished, some failed), failed / scan_error (the scan errored), expired (a byo scan never accepted), cancelled. Poll GET /api/scan/{id} until status is one of these terminal states.
Verdicts: clean, suspicious, malware, phishing, cloaking, unreachable.
Download an artifact — GET /api/scan/{id}/artifact/{aid}
curl -OJ https://whack.sh/api/scan/123/artifact/456 \ -H "Authorization: Bearer $WHACK_KEY"
Returns the HAR (JSON) or screenshot (PNG). Captured malware bodies are retained server-side and are never served over the API.
Download everything — GET /api/scan/{id}/bundle
curl -OJ https://whack.sh/api/scan/123/bundle \ -H "Authorization: Bearer $WHACK_KEY"
A zip of every HAR + screenshot + a report.md (verdict, per-hop WorldIP intel, billing). Payload bodies are excluded. For a single datacenter leg the zip contains exactly: datacenter-har.har, datacenter-screenshot.png, report.md, iocs.csv, iocs.stix.json. General rule: one {egress}-har.har + one {egress}-screenshot.png PER leg, plus report.md, iocs.csv, and iocs.stix.json.
Export IOCs — GET /api/scan/{id}/iocs
curl -OJ "https://whack.sh/api/scan/123/iocs?format=stix" \ -H "Authorization: Bearer $WHACK_KEY"
Indicators extracted from the scan — URLs, domains, IPs (with ASN/PTR), and captured-payload SHA-256s — as format=csv (default), stix (STIX 2.1 bundle) or misp (MISP event). CSV columns: type,value,context,first_seen. STIX is a 2.1 bundle with an identity object for "whack.sh". MISP is an Event object with Event.info and Attribute[]. The same export is on the report page, and iocs.csv + iocs.stix.json ship inside the bundle zip.
Errors
| HTTP | error | meaning |
|---|---|---|
| 401 | unauthorized | missing or invalid key |
| 400 | bad_url | missing, malformed, non-http(s), or >2048-char URL |
| 400 | blocked | an internal/private/reserved target (SSRF guard) |
| 402 | insufficient_balance | not enough credits; response also has max_credits + balance |
| 402 | daily_cap / weekly_cap / monthly_cap | your spend limit was hit; response has a human message (NOT a balance). Adjust limits in Account → Billing → Spend limits |
| 403 | sensitive_target | government/military/financial target restricted pending authorization (response has category) |
| 403 | byo_not_entitled | BYO needs Team Hunter or above |
| 403 | verification_required | account must be verified for high-volume use (response has verify_url) |
| 409 | byo_offline | your BYO agent isn't connected |
| 429 | rate_limited | too fast; response has retry_after (seconds) and a Retry-After header |
| 500 | error | transient server error; retry |
Limits & guardrails
Free datacenter scanning runs under standard rate limits; paid egress is governed by the daily and monthly credit caps you set, with velocity anomaly detection that auto-pauses a key on a spike. Each capture is hard-capped by cap_blocks — the agent stops pulling bytes the instant the cap is reached, so a heavy or hostile page can't run up your bill. Government, military & financial-sector targets are restricted pending authorization (403 sensitive_target).
whack