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",
"url": "https://paypal.com.account-verify.example",
"final_url": "https://paypal.com.account-verify.example",
"host": "paypal.com.account-verify.example",
"classification": "phish",
"reasons": [
"Impersonates a trusted brand",
"Domain registered 5 days ago"
],
"target_brand": "PayPal",
"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",
"url": "https://roblox.com.glogin-secure.example",
"final_url": "https://roblox.com.glogin-secure.example",
"host": "roblox.com.glogin-secure.example",
"classification": "phish",
"reasons": [
"Impersonates a trusted brand",
"Domain registered 3 days ago"
],
"target_brand": "Roblox",
"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}. |
url | string | The URL you submitted. |
final_url | string | The URL actually classified, after following redirects. |
host | string | Hostname of final_url. |
classification | string · null | The verdict — see values below. |
reasons | string[] | Human-readable explanations for the verdict. May be empty. |
target_brand | string · optional | The brand being impersonated, when one is identified (e.g. Roblox). |
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.
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"} |
| 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. |
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",
"url": "https://does-not-resolve.example",
"final_url": "https://does-not-resolve.example",
"host": "does-not-resolve.example",
"classification": null,
"reasons": ["Domain does not resolve (DNS lookup failed)"],
"scanned_at": "2026-06-06T15:23:45Z"
}