GrizzlyGrizzly

Overview

Grizzly is a phishing-detection API. Send it any URL and it classifies whether the page is a deceptive login or credential-harvesting attempt, returning a verdict plus a few human-readable reasons.

It's built for developers, security integrators, email gateways, and SOC tooling — anywhere you need to check URLs programmatically. The same classification engine powers the Grizzly web app and Chrome extension.

All requests go to https://api.grizzlysec.com. The free tier includes 10 scans per day, intended for evaluation and personal use. For higher volume, get in touch.

Quickstart

1. Get an API key

Sign in with Google or GitHub, then create an API key from your dashboard. The key is shown once and starts with gk_live_ — store it somewhere safe.

2. Make your first scan

Send a URL to the scan endpoint with your key in the Authorization header:

curl https://api.grizzlysec.com/scan \
  -H "Authorization: Bearer gk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"url": "https://paypal.com.account-verify.example"}'

3. Read the response

You get back a verdict in classification and an explanation in reasons:

{
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "input_url": "https://paypal.com.account-verify.example",
  "url": "https://paypal.com.account-verify.example",
  "host": "paypal.com.account-verify.example",
  "classification": "phish",
  "reasons": [
    { "code": "domain_squatting", "message": "Domain name closely resembles PayPal's real domain" },
    { "code": "platform_brand_mismatch", "message": "Hosted outside PayPal's infrastructure" }
  ],
  "target_brand": "PayPal",
  "page_intention": "credential_collection",
  "page_description": "Sign-in page requesting a PayPal email and password.",
  "screenshot_url": "https://api.grizzlysec.com/scan/550e8400-e29b-41d4-a716-446655440000/screenshot",
  "screenshot_expires_at": "2026-07-06T15:23:45Z",
  "scanned_at": "2026-06-06T15:23:45Z"
}

Authentication

Grizzly uses bearer tokens. Pass your API key in the Authorization header on every request:

Authorization: Bearer gk_live_...

Keys are issued per user and can be rotated from your dashboard. Treat your key like a password: use it only from server-side code, and never ship it in a browser or mobile app. A missing or invalid key returns 401 (see Errors).

API reference

POST https://api.grizzlysec.com/scan

Submit a URL for classification.

Request body

FieldTypeDescription
urlstring · requiredThe URL to scan. If you omit the scheme, https:// is assumed.
scan_modestring · optionalOne of auto (default), fast, or browser. auto runs a fast HTTP check and falls back to a full browser render when needed; fast skips the browser; browser forces a full render.

Response

A successful request returns 200 with a JSON object:

{
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "input_url": "https://roblox.com.glogin-secure.example",
  "url": "https://roblox.com.glogin-secure.example",
  "host": "roblox.com.glogin-secure.example",
  "classification": "phish",
  "reasons": [
    { "code": "domain_squatting", "message": "Domain name closely resembles Roblox's real domain" },
    { "code": "dns_ns_suffix_match", "message": "DNS nameservers do not match Roblox's known infrastructure" }
  ],
  "target_brand": "Roblox",
  "page_intention": "credential_collection",
  "page_description": "Login form prompting for a Roblox username and password.",
  "screenshot_url": "https://api.grizzlysec.com/scan/550e8400-e29b-41d4-a716-446655440000/screenshot",
  "screenshot_expires_at": "2026-07-06T15:23:45Z",
  "scanned_at": "2026-06-06T15:23:45Z"
}
FieldTypeDescription
scan_idstringUnique scan ID. Also the path of the shareable result page: grizzlysec.com/scan/{scan_id}.
input_urlstringThe URL you submitted.
urlstringThe URL the verdict applies to. A scan may evaluate multiple pages (e.g. across a redirect chain); Grizzly returns the most severe classification, and urlis that page's URL — not necessarily the final one.
hoststringHostname of the classified URL.
classificationstring · nullThe verdict — see values below.
reasonsobject[]Explanations for the verdict. Each entry is { code, message } — a stable indicator code and a human-readable message. May be empty.
target_brandstring · optionalThe brand being impersonated, when one is identified (e.g. Roblox).
page_intentionstring · optionalEnum code for the page's apparent goal, when identified (e.g. credential_collection, pii_collection).
page_descriptionstring · optionalA short (≤20-word) factual summary of the page. Present only when the page was fetched and read (browser scans); absent otherwise.
screenshot_urlstring · optionalLink to the page screenshot captured during the scan. Present only when a browser pass ran — fast scans and scans that never loaded a page have no pixels to return. See Screenshots.
screenshot_expires_atstring · optionalISO-8601 UTC timestamp after which screenshot_url stops serving the image. Accompanies screenshot_urlwhenever it's present.
scanned_atstringISO-8601 UTC timestamp.

Verdict values

  • phish — confirmed phishing or credential harvesting.
  • suspicious — suspicious signals, but not confirmed.
  • neutral — no phishing signals detected (e.g. a non-login page).
  • valid — a verified, trusted domain.
  • null — the scan ran but couldn't reach a verdict (see Errors).

Rate limits

The free tier allows 10 scans per day, reset at 00:00 UTC. Only successful (2xx) scans count — failed requests don't consume quota. Every response includes:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1717891200

X-RateLimit-Reset is a Unix timestamp for the next reset. Exceeding the limit returns 429 with a Retry-After header.

Screenshots

When a scan runs a full browser pass, Grizzly captures a screenshot of the page as it rendered and returns a screenshot_urlalongside the verdict. It's the visual evidence behind the classification — useful for analyst review, ticket attachments, and audit trails.

GET https://api.grizzlysec.com/scan/{scan_id}/screenshot

Authenticate exactly as you do for scanning. The endpoint responds 302 with a short-lived redirect to the image, so use a client that follows redirects:

curl -L -o screenshot.png \
  https://api.grizzlysec.com/scan/550e8400-e29b-41d4-a716-446655440000/screenshot \
  -H "Authorization: Bearer gk_live_..."

The final response is a image/png of the browser viewport at the moment the page was classified.

Availability

  • Browser scans only. A screenshot exists only when a browser pass actually rendered the page. fast scans, and scans that ended in a DNS or TLS failure, return no screenshot_url at all.
  • Owner-only.You can fetch screenshots for your own scans. Someone else's scan_id returns 404, the same as an unknown one.
  • Available for 30 days from the time of the scan, given by screenshot_expires_at. After that the endpoint returns 410. If you need the image long-term, download and store it on your side before then.
  • Doesn't consume quota.Fetching a screenshot isn't a scan and doesn't count against your daily limit.

Handling the link

Request screenshot_url when you need the image rather than caching the redirect it hands back — the storage URL it points to is signed and expires within minutes, while screenshot_urlitself keeps working for the full 30 days. It requires your API key, so it can't be dropped into an <img> tag in a browser; fetch it server-side and re-serve the bytes.

Screenshot capture is best-effort and runs slightly behind the verdict, so a screenshot_url can briefly 404 immediately after a scan, and a small number of scans never produce one. Treat the image as optional and retry once after a moment before giving up.

Errors

Errors use standard HTTP status codes, and the body has the shape { "detail": ... }.

StatusMeaning
200Success. A scan that runs but can't reach a verdict also returns 200, with classification: null and an explanation in reasons.
401Unauthorized — missing or invalid API key. {"detail":"unauthorized"}
404Screenshot endpoint only. Either the scan doesn't exist or isn't yours ({"detail":"scan_not_found"} — the two are deliberately indistinguishable), or the scan has no screenshot ({"detail":"screenshot_unavailable"}).
410Screenshot endpoint only. The 30-day window has passed. {"detail":"screenshot_expired"}
422Invalid request body (e.g. a missing url). detail lists the validation problems.
429Rate limit exceeded. {"detail":"rate_limit_exceeded"}, with a Retry-After header.
500Internal error. Usually transient — retry the request.
503Screenshot storage temporarily unavailable. {"detail":"storage_unavailable"} — retry.

Terminal scan errors

When a scan completes but can't classify the page — a DNS failure, TLS error, or timeout — you still get 200 with classification: null and a human-readable reason:

{
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "input_url": "https://does-not-resolve.example",
  "url": "https://does-not-resolve.example",
  "host": "does-not-resolve.example",
  "classification": null,
  "reasons": [
    { "code": "dns_not_resolved", "message": "Domain does not resolve (DNS lookup failed)" }
  ],
  "scanned_at": "2026-06-06T15:23:45Z"
}