← zerophish

API documentation

Classify URLs, search your scan history, and pull threat-intel feeds over HTTP / JSON.

Getting started

The ZerophishEx API is a JSON-over-HTTPS API rooted at http://zerophish.ai/api. All responses are application/json. The scanning endpoints accept anonymous requests (heavily IP rate-limited); everything else needs an API key.

The machine-readable contract lives at /api/openapi.json (OpenAPI 3.1) — import it into Postman, Insomnia, Swagger UI, or generate a client with openapi-generator.

Quick start (Python)

import requests, time

API_KEY = "zp_live_…"
H = {"Authorization": f"Bearer {API_KEY}"}

# 1. submit a URL
r = requests.post("http://zerophish.ai/api/scan", headers=H, json={"url": "https://example.com"})
data = r.json()

if data.get("cached"):
    print("cached:", data["prediction"]["verdict"])
else:
    job_id = data["job_id"]
    # 2. poll until the scan completes
    while True:
        r = requests.get(f"http://zerophish.ai/api/scan/{job_id}", headers=H)
        if r.status_code == 202:
            time.sleep(2); continue
        print(r.json())
        break

Authentication

Mint an API key in workspace settings → API keys. Send it as a bearer token on every authenticated request:

Authorization: Bearer zp_live_xxxxxxxxxxxxxxxxxxxxxxxx

Keys carry one or more scopes:

ScopeGrants
scan Submit new URLs for scanning (POST /api/scan).
read Read scan results, search, lookup and feeds.
admin Reserved for future privileged endpoints.

Organisation scoping is automatic. /api/search, /api/lookup and /api/feeds/stix.json return data for the workspace that owns the API key you send. Because a key is minted inside a workspace, it is already tied to that organisation — there is no org parameter to set. To query a different organisation, use a key from that workspace.

A request that lacks the required scope returns 403 with body error: insufficient_scope. Anonymous calls to the scanning endpoints are allowed but limited; /api/search, /api/lookup and the STIX feed require a key.

API key for testing

Paste a key here to use the Try it buttons below. It is kept only in your browser (this field + optional localStorage) and sent directly to this origin — never logged or transmitted elsewhere.

POST /api/scan optional · scope: scan

Submit a URL for scanning

Enqueues a phishing scan for a URL. By default a recent cached result is returned immediately if one exists; otherwise a background job is started and a `job_id` is returned that you poll with `GET /api/scan/{id}`. Anonymous calls are allowed but rate-limited by IP; authenticated calls get a higher per-key/per-org quota.

Parameters

NameInTypeRequiredDescription
url body string yes The absolute URL to scan (http/https).
fresh body boolean no Skip the cache and force a new scan. Accepts true/1/"true".
deep body boolean no Run a deep scan (headless browser, HAR, cert + network depth). Lower per-org quota. Alias: deep_scan.

Code samples

curl -X POST http://zerophish.ai/api/scan \
  -H "Authorization: Bearer $ZEROPHISH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"deep":false,"url":"https://example.com"}'
import requests

API_KEY = "zp_live_…"  # from /app/settings/api-keys

resp = requests.post(
    "http://zerophish.ai/api/scan",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"deep": False, "url": "https://example.com"},
)
print(resp.status_code)
print(resp.json())
const API_KEY = "zp_live_…"; // from /app/settings/api-keys

const resp = await fetch("http://zerophish.ai/api/scan", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({"deep":false,"url":"https://example.com"}),
});
const data = await resp.json();
console.log(resp.status, data);

Responses

202 Scan enqueued. Poll `GET /api/scan/{job_id}`.
{
  "cached": false,
  "deep": false,
  "job_id": 918273
}
200 A recent cached result was returned (no new scan started).
{
  "cached": true,
  "prediction": {
    "brands": "Example Bank",
    "campaign_id": null,
    "confidence": "high",
    "duration_ms": 8421,
    "id": "b3b1f0e2-2c4a-4f9d-9d2e-1a2b3c4d5e6f",
    "jarm": "27d40d40d29d40d1dc42d43d00041d...",
    "kit_matches": [],
    "normalized_url": "login.examplebank.com.verify-account.example/",
    "phishing": true,
    "phishing_score": 0.94,
    "redirect_chain": [],
    "scan_depth": "quick",
    "screenshot_name": "b3b1f0e2.png",
    "signals": [
      {
        "detail": "Brand appears as a subdomain label.",
        "name": "brand_in_subdomain",
        "severity": "high"
      },
      {
        "detail": "Password field posts cross-origin.",
        "name": "credential_form",
        "severity": "high"
      }
    ],
    "similar_brands": [
      {
        "brand": "Example Bank",
        "score": 0.91
      }
    ],
    "summary": "Credential-harvesting page impersonating Example Bank's login. The domain embeds the brand as a subdomain of an unrelated registrable domain.",
    "suspicious_domain": true,
    "tags": [
      "credential-harvesting"
    ],
    "url": "https://login.examplebank.com.verify-account.example/",
    "url_parts": {
      "host": "login.examplebank.com.verify-account.example",
      "registrable_domain": "verify-account.example",
      "scheme": "https"
    },
    "verdict": "phishing"
  },
  "scanned_at": "2026-05-30T12:01:55.000000Z"
}
400 `url` was missing or empty.
{
  "error": "url is required"
}
403 API key lacks the `scan` scope.
{
  "error": "insufficient_scope",
  "required": "scan"
}
422 The scan job could not be enqueued.
{
  "error": "Failed to start scan",
  "reason": "..."
}
429 Daily request limit exceeded.
{
  "error": "Daily request limit exceeded"
}

Try it

GET /api/scan/:id optional · scope: read

Fetch scan result / status by job id

Returns the completed `Prediction` for a job id, or a status object while it is still running. Use the `job_id` returned by `POST /api/scan`. Returns the prediction object directly on success.

Parameters

NameInTypeRequiredDescription
id path integer yes The job id returned by POST /api/scan.

Code samples

curl "http://zerophish.ai/api/scan/918273" \
  -H "Authorization: Bearer $ZEROPHISH_API_KEY"
import requests

API_KEY = "zp_live_…"  # from /app/settings/api-keys

resp = requests.get(
    "http://zerophish.ai/api/scan/918273",
    headers={"Authorization": f"Bearer {API_KEY}"},
)
print(resp.status_code)
print(resp.json())
const API_KEY = "zp_live_…"; // from /app/settings/api-keys

const resp = await fetch("http://zerophish.ai/api/scan/918273", {
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
  },
});
const data = await resp.json();
console.log(resp.status, data);

Responses

200 Scan completed — the prediction object is returned.
{
  "brands": "Example Bank",
  "campaign_id": null,
  "confidence": "high",
  "duration_ms": 8421,
  "id": "b3b1f0e2-2c4a-4f9d-9d2e-1a2b3c4d5e6f",
  "jarm": "27d40d40d29d40d1dc42d43d00041d...",
  "kit_matches": [],
  "normalized_url": "login.examplebank.com.verify-account.example/",
  "phishing": true,
  "phishing_score": 0.94,
  "redirect_chain": [],
  "scan_depth": "quick",
  "screenshot_name": "b3b1f0e2.png",
  "signals": [
    {
      "detail": "Brand appears as a subdomain label.",
      "name": "brand_in_subdomain",
      "severity": "high"
    },
    {
      "detail": "Password field posts cross-origin.",
      "name": "credential_form",
      "severity": "high"
    }
  ],
  "similar_brands": [
    {
      "brand": "Example Bank",
      "score": 0.91
    }
  ],
  "summary": "Credential-harvesting page impersonating Example Bank's login. The domain embeds the brand as a subdomain of an unrelated registrable domain.",
  "suspicious_domain": true,
  "tags": [
    "credential-harvesting"
  ],
  "url": "https://login.examplebank.com.verify-account.example/",
  "url_parts": {
    "host": "login.examplebank.com.verify-account.example",
    "registrable_domain": "verify-account.example",
    "scheme": "https"
  },
  "verdict": "phishing"
}
202 Still running or retrying.
{
  "status": "in_progress"
}
400 Job id was not an integer.
{
  "error": "Invalid job ID"
}
404 No job with that id.
{
  "error": "Job not found"
}
422 The scan failed and was discarded.
{
  "error": "fetch failed: timeout"
}

Try it

GET /api/lookup required · scope: read

Look up the latest known scan for a URL

Returns the most recent scan your organisation has for a normalised URL without starting a new scan. The organisation is taken from the API key automatically — no org parameter needed. Requires the `read` scope.

Parameters

NameInTypeRequiredDescription
url query string yes The URL to look up.

Code samples

curl "http://zerophish.ai/api/lookup?url=https%3A%2F%2Fexample.com" \
  -H "Authorization: Bearer $ZEROPHISH_API_KEY"
import requests

API_KEY = "zp_live_…"  # from /app/settings/api-keys

resp = requests.get(
    "http://zerophish.ai/api/lookup",
    headers={"Authorization": f"Bearer {API_KEY}"},
    params={"url": "https://example.com"},
)
print(resp.status_code)
print(resp.json())
const API_KEY = "zp_live_…"; // from /app/settings/api-keys

const resp = await fetch("http://zerophish.ai/api/lookup?url=https%3A%2F%2Fexample.com", {
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
  },
});
const data = await resp.json();
console.log(resp.status, data);

Responses

200 Known URL — cached prediction returned.
{
  "cached": true,
  "prediction": {
    "brands": "Example Bank",
    "campaign_id": null,
    "confidence": "high",
    "duration_ms": 8421,
    "id": "b3b1f0e2-2c4a-4f9d-9d2e-1a2b3c4d5e6f",
    "jarm": "27d40d40d29d40d1dc42d43d00041d...",
    "kit_matches": [],
    "normalized_url": "login.examplebank.com.verify-account.example/",
    "phishing": true,
    "phishing_score": 0.94,
    "redirect_chain": [],
    "scan_depth": "quick",
    "screenshot_name": "b3b1f0e2.png",
    "signals": [
      {
        "detail": "Brand appears as a subdomain label.",
        "name": "brand_in_subdomain",
        "severity": "high"
      },
      {
        "detail": "Password field posts cross-origin.",
        "name": "credential_form",
        "severity": "high"
      }
    ],
    "similar_brands": [
      {
        "brand": "Example Bank",
        "score": 0.91
      }
    ],
    "summary": "Credential-harvesting page impersonating Example Bank's login. The domain embeds the brand as a subdomain of an unrelated registrable domain.",
    "suspicious_domain": true,
    "tags": [
      "credential-harvesting"
    ],
    "url": "https://login.examplebank.com.verify-account.example/",
    "url_parts": {
      "host": "login.examplebank.com.verify-account.example",
      "registrable_domain": "verify-account.example",
      "scheme": "https"
    },
    "verdict": "phishing"
  },
  "scanned_at": "2026-05-30T12:01:55.000000Z",
  "status": "known"
}
200 Unknown URL — never scanned by your org.
{
  "status": "unknown"
}
400 `url` query param missing.
{
  "error": "url_required"
}
403 Missing/invalid API key, or the key lacks the `read` scope.
{
  "error": "authentication_required"
}

Try it

Needs an API key (paste one above). Its workspace is used automatically as the organisation — nothing else to set.

GET /api/public/lookup no auth

Look up a publicly-visible scan for a URL (no auth)

Unauthenticated lookup against the corpus of public scans. IP rate-limited to 60/day. Does not start a scan. No API key required.

Parameters

NameInTypeRequiredDescription
url query string yes The URL to look up.

Code samples

curl "http://zerophish.ai/api/public/lookup?url=https%3A%2F%2Fexample.com"
import requests

API_KEY = "zp_live_…"  # from /app/settings/api-keys

resp = requests.get(
    "http://zerophish.ai/api/public/lookup",
    params={"url": "https://example.com"},
)
print(resp.status_code)
print(resp.json())
const API_KEY = "zp_live_…"; // from /app/settings/api-keys

const resp = await fetch("http://zerophish.ai/api/public/lookup?url=https%3A%2F%2Fexample.com", {
});
const data = await resp.json();
console.log(resp.status, data);

Responses

200 Known public URL.
{
  "cached": true,
  "prediction": {
    "brands": "Example Bank",
    "campaign_id": null,
    "confidence": "high",
    "duration_ms": 8421,
    "id": "b3b1f0e2-2c4a-4f9d-9d2e-1a2b3c4d5e6f",
    "jarm": "27d40d40d29d40d1dc42d43d00041d...",
    "kit_matches": [],
    "normalized_url": "login.examplebank.com.verify-account.example/",
    "phishing": true,
    "phishing_score": 0.94,
    "redirect_chain": [],
    "scan_depth": "quick",
    "screenshot_name": "b3b1f0e2.png",
    "signals": [
      {
        "detail": "Brand appears as a subdomain label.",
        "name": "brand_in_subdomain",
        "severity": "high"
      },
      {
        "detail": "Password field posts cross-origin.",
        "name": "credential_form",
        "severity": "high"
      }
    ],
    "similar_brands": [
      {
        "brand": "Example Bank",
        "score": 0.91
      }
    ],
    "summary": "Credential-harvesting page impersonating Example Bank's login. The domain embeds the brand as a subdomain of an unrelated registrable domain.",
    "suspicious_domain": true,
    "tags": [
      "credential-harvesting"
    ],
    "url": "https://login.examplebank.com.verify-account.example/",
    "url_parts": {
      "host": "login.examplebank.com.verify-account.example",
      "registrable_domain": "verify-account.example",
      "scheme": "https"
    },
    "verdict": "phishing"
  },
  "scanned_at": "2026-05-30T12:01:55.000000Z",
  "status": "known"
}
200 Unknown URL.
{
  "status": "unknown"
}
400 `url` query param missing.
{
  "error": "url_required"
}
429 Rate limited (60/day per IP).
{
  "error": "rate_limited",
  "limit": 60
}

Try it

GET /api/feeds/stix.json required · scope: read

STIX 2.1 indicator feed of confirmed phishing

Exports your organisation's confirmed-phishing URLs from the last 90 days as a STIX 2.1 bundle of `indicator` objects — drop straight into a TIP. The organisation is taken from the API key automatically. Requires the `read` scope.

Code samples

curl "http://zerophish.ai/api/feeds/stix.json" \
  -H "Authorization: Bearer $ZEROPHISH_API_KEY"
import requests

API_KEY = "zp_live_…"  # from /app/settings/api-keys

resp = requests.get(
    "http://zerophish.ai/api/feeds/stix.json",
    headers={"Authorization": f"Bearer {API_KEY}"},
)
print(resp.status_code)
print(resp.json())
const API_KEY = "zp_live_…"; // from /app/settings/api-keys

const resp = await fetch("http://zerophish.ai/api/feeds/stix.json", {
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
  },
});
const data = await resp.json();
console.log(resp.status, data);

Responses

200 A STIX 2.1 bundle.
{
  "id": "bundle--6f1d…",
  "objects": [
    {
      "confidence": 90,
      "id": "indicator--b3b1f0e2-2c4a-4f9d-9d2e-1a2b3c4d5e6f",
      "indicator_types": [
        "malicious-activity"
      ],
      "labels": [
        "phishing"
      ],
      "pattern": "[url:value = 'https://login.examplebank.com.verify-account.example/']",
      "pattern_type": "stix",
      "spec_version": "2.1",
      "type": "indicator"
    }
  ],
  "type": "bundle"
}
403 Missing/invalid API key, or the key lacks the `read` scope.
{
  "error": "authentication_required"
}

Try it

Needs an API key (paste one above). Its workspace is used automatically as the organisation — nothing else to set.