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".

paramdefaultnotes
urlrequired · http(s) · ≤ 2048 chars
egressdatacentercomma list of datacenter,residential,mobile,byo — run in parallel for the Split-Horizon Diff
cap_blocks1bandwidth 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
geous2-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
profilerandomdesktop persona for the datacenter / residential capture
mobile_osandroidandroid · 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.

egressbase (incl. 5 MB)overage
datacenter0 — free+1 cr / MB
datacenter · geo (non-US geo)0 — free+1 cr / MB
residential5 cr+1 cr / MB
mobile10 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}
    ]
  }
}
fieldmeaning
idnumeric scan id (use it in the path)
urlthe scanned URL
egress_setJSON array of the egress legs that ran
cap_blocksthe chosen byte-cap (1/2/3/5/10/20 blocks = 5/10/15/25/50/100 MB)
statuslifecycle (see status list below)
verdictone of clean, suspicious, malware, phishing, cloaking, unreachable (null until finished)
cloaking_score0-100 divergence score across the egress legs (higher = more cloaking); null until finished
credits_chargedactual credits billed (0 for a free datacenter scan)
max_creditsthe pre-charge ceiling
created_at / finished_atunix epoch seconds (UTC); finished_at null until terminal
errora short error string when status is failed/scan_error, else null
report_tokenopens the human report at /app/report/{report_token} in the dashboard (session login)
share_token / share_enabled / share_detailset 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) → claimedrunningdone. 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

HTTPerrormeaning
401unauthorizedmissing or invalid key
400bad_urlmissing, malformed, non-http(s), or >2048-char URL
400blockedan internal/private/reserved target (SSRF guard)
402insufficient_balancenot enough credits; response also has max_credits + balance
402daily_cap / weekly_cap / monthly_capyour spend limit was hit; response has a human message (NOT a balance). Adjust limits in Account → Billing → Spend limits
403sensitive_targetgovernment/military/financial target restricted pending authorization (response has category)
403byo_not_entitledBYO needs Team Hunter or above
403verification_requiredaccount must be verified for high-volume use (response has verify_url)
409byo_offlineyour BYO agent isn't connected
429rate_limitedtoo fast; response has retry_after (seconds) and a Retry-After header
500errortransient 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).