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
| Field | Type | Description |
|---|---|---|
url | string · required | The URL to scan. If you omit the scheme, https:// is assumed. |
scan_mode | string · optional | One 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"
}| Field | Type | Description |
|---|---|---|
scan_id | string | Unique scan ID. Also the path of the shareable result page: grizzlysec.com/scan/{scan_id}. |
input_url | string | The URL you submitted. |
url | string | The 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. |
host | string | Hostname of the classified URL. |
classification | string · null | The verdict — see values below. |
reasons | object[] | Explanations for the verdict. Each entry is { code, message } — a stable indicator code and a human-readable message. May be empty. |
target_brand | string · optional | The brand being impersonated, when one is identified (e.g. Roblox). |
page_intention | string · optional | Enum code for the page's apparent goal, when identified (e.g. credential_collection, pii_collection). |
page_description | string · optional | A short (≤20-word) factual summary of the page. Present only when the page was fetched and read (browser scans); absent otherwise. |
screenshot_url | string · optional | Link 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_at | string · optional | ISO-8601 UTC timestamp after which screenshot_url stops serving the image. Accompanies screenshot_urlwhenever it's present. |
scanned_at | string | ISO-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: 1717891200X-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.
fastscans, and scans that ended in a DNS or TLS failure, return noscreenshot_urlat all. - Owner-only.You can fetch screenshots for your own scans. Someone else's
scan_idreturns404, 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 returns410. 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": ... }.
| Status | Meaning |
|---|---|
| 200 | Success. A scan that runs but can't reach a verdict also returns 200, with classification: null and an explanation in reasons. |
| 401 | Unauthorized — missing or invalid API key. {"detail":"unauthorized"} |
| 404 | Screenshot 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"}). |
| 410 | Screenshot endpoint only. The 30-day window has passed. {"detail":"screenshot_expired"} |
| 422 | Invalid request body (e.g. a missing url). detail lists the validation problems. |
| 429 | Rate limit exceeded. {"detail":"rate_limit_exceeded"}, with a Retry-After header. |
| 500 | Internal error. Usually transient — retry the request. |
| 503 | Screenshot 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"
}