Phishing link checker API
`GET /v1/url` checks where a link lands after shorteners and redirects. It checks the destination domain's age and brand imitation, plus phishing and malware URL feeds. It returns a `verdict` of scam, suspicious, safe or unknown with `reason_codes`. When the host is a top site, a business's own site, a URL shortener or a platform anyone can publish on, Bewary lists the exact link instead of the whole host. A call costs 1 credit (30 with deep=true), a free key gets 1,000 credits a month, no card, and it answers in median 477 ms.
- Endpoint
- GET
https://api.bewary.app/v1/url - Price
- 1 credit, deep=true 30
- Free key
- 1,000 credits a month, no card
- Speed
- Median 477 ms, measured
- Returns
- verdict, p_scam, entities with typed facts, reason codes
- MCP tool
lookup_url- Free tool
- Is this link safe?
Try it
Free from this page: 10 lookups a dayCall it
curl "https://api.bewary.app/v1/url?u=http%3A%2F%2Fpaypal-login-secure.top%2Fverify%3Faccount%3Dupdate" import requests
r = requests.get("https://api.bewary.app/v1/url?u=http%3A%2F%2Fpaypal-login-secure.top%2Fverify%3Faccount%3Dupdate", timeout=10)
result = r.json()
print(result["verdict"], result["p_scam"], result["reason_codes"]) const res = await fetch("https://api.bewary.app/v1/url?u=http%3A%2F%2Fpaypal-login-secure.top%2Fverify%3Faccount%3Dupdate")
const result = await res.json()
console.log(result.verdict, result.p_scam, result.reason_codes) Act on the result
Copy-paste code that turns the answer into a decision. It runs with or without BEWARY_API_KEY. TypeScript uses bewary.d.ts.
import os, requests
def link_action(url):
"""block, warn or allow, for a link preview or before an agent opens a page"""
key = os.environ["BEWARY_API_KEY"] # a free key: bewary.app/account
headers = {"Authorization": f"Bearer {key}"}
try:
r = requests.get("https://api.bewary.app/v1/url", params={"u": url},
headers=headers, timeout=10)
r.raise_for_status()
except requests.RequestException:
return "warn" # never fail open
verdict = r.json()["verdict"]
return {"scam": "block", "safe": "allow"}.get(verdict, "warn")
print(link_action("http://paypal-login-secure.top/verify")) # block import type { Envelope } from "./bewary"
export async function linkAction(url: string): Promise<"block" | "warn" | "allow"> {
const key = process.env.BEWARY_API_KEY! // a free key: bewary.app/account
const headers: Record<string, string> = { Authorization: `Bearer ${key}` }
try {
const res = await fetch("https://api.bewary.app/v1/url?u=" + encodeURIComponent(url),
{ headers, signal: AbortSignal.timeout(10_000) })
if (!res.ok) return "warn" // never fail open
const r: Envelope = await res.json()
return r.verdict === "scam" ? "block" : r.verdict === "safe" ? "allow" : "warn"
} catch { return "warn" }
} Response
The fields you will use
verdict- The link's verdict, judged together with its domain.
entities[0].facts.final_url- Where the link really lands.
entities[1].facts.lookalike- The brand its domain imitates, and how.
entities[1].facts.age_days- How old the domain is.
reason_codes- Why, strongest first.
{ "schema_version": "1.7.0", "request_id": "req_01M69WHC5ZP5H7M54R0HKK97QD", "checked_at": "2026-10-31T21:06:05Z", "engine": "bewary-engine/3.0.0", "models": { "calibration": "interim-band-v0" }, "input": { "kind": "lookup", "channel": "unknown", "language": null, "region": null }, "verdict": "scam", "p_scam": 0.9116, "risk_score": 91, "confidence": 1, "classification": null, "reason_codes": [ "DOMAIN_BRAND_ON_ABUSED_TLD", "DOMAIN_LOOKALIKE_BRAND_WORD" ], "entities": [ { "id": "e1", "type": "url", "value": "http://paypal-login-secure.top/verify?account=update", "normalized": "http://paypal-login-secure.top/verify?account=update", "role": "link_target", "verdict": "scam", "p_scam": 0.9116, "risk_score": 91, "reason_codes": [ "DOMAIN_BRAND_ON_ABUSED_TLD", "DOMAIN_LOOKALIKE_BRAND_WORD" ], "facts": { "scheme": "http", "host": "e2", "final_url": "http://paypal-login-secure.top/verify?account=update", "redirect_chain": null, "shortener": false, "redirect_resolved": true, "short_link": null, "page": null, "target_countries": null, "list_count": null, "platform_status": null }, "related": [ "e2" ], "datasets": [ { "dataset": "threat_feeds", "coverage": "WW", "in_coverage": true }, { "dataset": "threat_aggregates", "coverage": "WW", "in_coverage": true }, { "dataset": "web_pages", "coverage": "WW", "in_coverage": true }, { "dataset": "short_links", "coverage": "WW", "in_coverage": false }, { "dataset": "platform_pages", "coverage": "WW", "in_coverage": true }, { "dataset": "community_reports", "coverage": "WW", "in_coverage": true } ] }, { "id": "e2", "type": "domain", "value": "paypal-login-secure.top", "normalized": "paypal-login-secure.top", "role": "link_target", "verdict": "scam", "p_scam": 0.9116, "risk_score": 91, "reason_codes": [ "DOMAIN_BRAND_ON_ABUSED_TLD", "DOMAIN_LOOKALIKE_BRAND_WORD" ], "facts": { "registrable_domain": "paypal-login-secure.top", "registered_domain": "paypal-login-secure.top", "tld": "top", "tld_abuse": "high", "registered_at": null, "expires_at": null, "age_days": null, "age_bucket": "unknown", "registrar": null, "dns": { "resolves": false, "a": null, "mx": null, "ns": null }, "status": "gone", "cert_first_seen": null, "lookalike": { "brand_id": "paypal", "technique": "brand_word" }, "platform": "none", "allowlisted": false, "listed": false, "seen_in_checks_7d": null, "country": null, "target_countries": null, "list_count": null, "site": null, "platform_status": null, "registry_status": null, "hold": null, "registered": false, "registration_checked_at": "2026-10-30T19:18:58Z", "archive": { "checked_at": "2026-10-30T19:20:08Z", "captured": false, "first_capture_at": null, "last_capture_at": null, "captured_before_registration": null, "months_before_registration": null, "last_capture_before_registration_at": null }, "first_seen_by_bewary": null, "first_listed_publicly": null }, "related": [], "datasets": [ { "dataset": "registration", "coverage": "WW", "in_coverage": true }, { "dataset": "dns", "coverage": "WW", "in_coverage": true }, { "dataset": "certificates", "coverage": "WW", "in_coverage": true }, { "dataset": "web_archive", "coverage": "WW", "in_coverage": true }, { "dataset": "threat_feeds", "coverage": "WW", "in_coverage": true }, { "dataset": "threat_aggregates", "coverage": "WW", "in_coverage": true }, { "dataset": "brand_registry", "coverage": "WW", "in_coverage": true }, { "dataset": "platform_pages", "coverage": "WW", "in_coverage": true }, { "dataset": "community_reports", "coverage": "WW", "in_coverage": true } ] } ], "signals": [ { "code": "DOMAIN_BRAND_ON_ABUSED_TLD", "entity": "e2", "severity": "high", "weight": 55, "source_class": "heuristic", "p": null, "dataset": "brand_registry", "coverage": "WW", "evidence": { "brand_id": "paypal", "tld": "top" } }, { "code": "DOMAIN_LOOKALIKE_BRAND_WORD", "entity": "e2", "severity": "medium", "weight": 35, "source_class": "heuristic", "p": null, "dataset": "brand_registry", "coverage": "WW", "evidence": {} } ], "degraded": false, "degraded_reasons": [], "cached": false, "explanation": null} { "schema_version": "1.7.0", "request_id": "req_01M69WHEJ40Q41YEYK4S67Q4FD", "checked_at": "2026-10-31T21:06:08Z", "engine": "bewary-engine/3.0.0", "models": { "calibration": "interim-band-v0" }, "input": { "kind": "lookup", "channel": "unknown", "language": null, "region": null }, "verdict": "safe", "p_scam": 0, "risk_score": 0, "confidence": 1, "classification": null, "reason_codes": [ "DOMAIN_ALLOWLISTED" ], "entities": [ { "id": "e1", "type": "url", "value": "https://www.usps.com/manage/", "normalized": "https://www.usps.com/manage/", "role": "link_target", "verdict": "safe", "p_scam": 0, "risk_score": 0, "reason_codes": [ "DOMAIN_ALLOWLISTED" ], "facts": { "scheme": "https", "host": "e2", "final_url": "https://www.usps.com/manage/", "redirect_chain": null, "shortener": false, "redirect_resolved": true, "short_link": null, "page": null, "target_countries": null, "list_count": null, "platform_status": null }, "related": [ "e2" ], "datasets": [ { "dataset": "threat_feeds", "coverage": "WW", "in_coverage": true }, { "dataset": "threat_aggregates", "coverage": "WW", "in_coverage": true }, { "dataset": "web_pages", "coverage": "WW", "in_coverage": true }, { "dataset": "short_links", "coverage": "WW", "in_coverage": false }, { "dataset": "platform_pages", "coverage": "WW", "in_coverage": true }, { "dataset": "community_reports", "coverage": "WW", "in_coverage": true } ] }, { "id": "e2", "type": "domain", "value": "www.usps.com", "normalized": "www.usps.com", "role": "link_target", "verdict": "safe", "p_scam": 0, "risk_score": 0, "reason_codes": [ "DOMAIN_ALLOWLISTED" ], "facts": { "registrable_domain": "usps.com", "registered_domain": "usps.com", "tld": "com", "tld_abuse": "low", "registered_at": "1997-07-10T04:00:00Z", "expires_at": "2027-07-09T04:00:00Z", "age_days": 10704, "age_bucket": "gt_1y", "registrar": "Network Solutions, LLC", "dns": { "resolves": true, "a": [ "2.16.193.16" ], "mx": null, "ns": [ "n2-a1.aka-ns.net", "n2-a13.aka-ns.net", "n2-a22.aka-ns.net", "n2-a28.aka-ns.net", "n2-a8.aka-ns.net", "n2-a9.aka-ns.net" ] }, "status": "live", "cert_first_seen": null, "lookalike": null, "platform": "none", "allowlisted": true, "listed": false, "seen_in_checks_7d": null, "country": null, "target_countries": null, "list_count": null, "site": null, "platform_status": null, "registry_status": [ "client_transfer_prohibited" ], "hold": "none", "registered": true, "registration_checked_at": "2026-10-30T19:19:06Z", "archive": { "checked_at": "2026-10-30T19:20:33Z", "captured": true, "first_capture_at": "1997-12-10T17:12:54Z", "last_capture_at": "2026-10-30T15:27:15Z", "captured_before_registration": false, "months_before_registration": null, "last_capture_before_registration_at": null }, "first_seen_by_bewary": null, "first_listed_publicly": null }, "related": [], "datasets": [ { "dataset": "registration", "coverage": "WW", "in_coverage": true }, { "dataset": "dns", "coverage": "WW", "in_coverage": true }, { "dataset": "certificates", "coverage": "WW", "in_coverage": true }, { "dataset": "web_archive", "coverage": "WW", "in_coverage": true }, { "dataset": "threat_feeds", "coverage": "WW", "in_coverage": true }, { "dataset": "threat_aggregates", "coverage": "WW", "in_coverage": true }, { "dataset": "brand_registry", "coverage": "WW", "in_coverage": true }, { "dataset": "platform_pages", "coverage": "WW", "in_coverage": true }, { "dataset": "community_reports", "coverage": "WW", "in_coverage": true } ] } ], "signals": [ { "code": "DOMAIN_ALLOWLISTED", "entity": "e2", "severity": "info", "weight": -60, "source_class": "first_party", "p": null, "dataset": "brand_registry", "coverage": "WW", "evidence": {} } ], "degraded": false, "degraded_reasons": [], "cached": false, "explanation": null} Real responses, captured from the live API on 2026-10-31. Every response has this shape: see Response format.
Link facts
Every key is in every response, null when unknown. A key marked Coming stays null until its source ships, so you can code against it now.
| Key | Type | Meaning |
|---|---|---|
scheme | http | https | other or null | The link's scheme. |
host | entity id or null | The domain entity this link points to. |
final_url | string or null | Where the link lands after shorteners and redirects. |
shortener | boolean or null | The link uses a URL shortener. |
redirect_resolved | boolean or null | A shortened link was followed to its destination. |
page | object or null | With deep=true: whether the page was read (analyzed) and which brand it shows (brand_shown). Its has_login_form and has_payment_form keys stay null for now. |
redirect_chain | array or null | The short links read on the way, in order. |
short_link | object or null | On a shortener: the link's state as the shortener answers it, such as disabled for abuse. |
platform_status | object or null | A form or link-in-bio platform's own answer about this URL, such as a takedown. |
target_countries | array or null | Countries whose people it is aimed at, from the national lists that name it (PL, TW, DE, US). |
list_count | integer or null | How many lists name it, ours and others'. |
Parameters
| Name | Type | Description |
|---|---|---|
uquery, required | string | The full URL, URL-encoded. Shorteners are followed. |
deepquery | boolean | Also read the destination page with a model (30 credits, API key required). Default false. |
Questions
Can I check a link before my app or agent opens it?
Yes, and the check never opens the suspicious site. Without deep=true, Bewary contacts only known link shorteners such as bit.ly, t.co and tinyurl.com to see where they point, and reads open-redirect targets from the URL itself. deep=true reads the destination page from our side, never from your app.
Does it expand shortened links?
Yes, GET /v1/url follows shorteners and redirects and returns the domain the link points to, with that domain's facts.
What makes a link a scam?
Mostly its domain: a brand name on a cheap, abused TLD (paypal-login-secure.top), a lookalike spelling, a domain registered days ago, or a listing on a threat list or our scam list. The reason codes name which.
How fast is it?
Measured median 477 ms for a link seen for the first time. Following a slow shortener can take a few seconds.
Used for
- Link previews in a chat, email or social app.
- Scanning URLs in user posts before they are published.
- An AI agent deciding whether a page is safe to open.
Reason codes it can return
| Code | Meaning | Severity |
|---|---|---|
LISTED_BLOCKLIST | On scam blocklist | Critical |
DOMAIN_ALLOWLISTED | Established domain (allowlist) | Info |
DOMAIN_AGE_GT_1Y | Established domain | Info |
DOMAIN_AGE_90D_1Y | Established domain (<1y) | Info |
DOMAIN_AGE_30_90D | Recently-registered domain | Medium |
DOMAIN_AGE_LT_30D | Brand-new domain | Medium |
DOMAIN_MULTITENANT_PLATFORM | Hosted on a public platform | Info |
DOMAIN_AGE_UNKNOWN | Domain age unknown | Info |
DOMAIN_LOOKALIKE_BRAND_WORD | Lookalike domain | Medium |
DOMAIN_MIXED_SCRIPT | Fake domain (mixed alphabets) | High |
DOMAIN_HOMOGLYPH_BRAND | Fake domain (homoglyph) | High |
DOMAIN_BRAND_ON_ABUSED_TLD | Fake domain (brand + suspicious TLD) | High |
DOMAIN_TYPO_BRAND | Possible fake domain (typo) | Medium |
DOMAIN_BRAND_SCAM_WORD | Possible fake domain (brand + scam word) | Medium |
TLD_HIGH_ABUSE | Risky domain | Medium |
URL_LISTED_THREAT_INTEL | Known dangerous link | Critical |
URL_HIGH_RISK_SCORE | High-risk link | High |
URL_SUSPICIOUS_SCORE | Suspicious link | Medium |
PAGE_MALICIOUS | Dangerous page | High |
DOMAIN_DECEPTIVE_STRUCTURE | A brand name placed before an @ so the real host hides after it | High |
DOMAIN_CERT_LT_48H | First TLS certificate issued in the last 48 hours | Medium |
URL_SHORTENER | Uses a link shortener | Info |
URL_REDIRECT_CROSS_DOMAIN | Redirects to a different domain | Medium |
LISTED_CAUTION | On a broad threat list that also holds spam and malware hosts | Medium |
PROTECTIVE_DNS_BLOCKED | Blocked by public protective DNS resolvers | Medium |
PLATFORM_TAKEDOWN | The hosting platform took it down | High |
PLATFORM_WARNING | The platform shows a phishing or malware warning in front of it | High |
DOMAIN_REGISTRY_HOLD | The registry holds the domain out of the DNS | High |
DOMAIN_REGISTRAR_HOLD | The registrar suspended the domain | Medium |
SHORTLINK_DISABLED | The link shortener disabled the link | High |
SHORTLINK_WARNING | The link shortener warns before the destination | Medium |
PAGE_CREDENTIAL_HARVEST | The page asks for a login it should not | High |
PAGE_PAYMENT_HARVEST | The page asks for card or bank details it should not | High |
Reserved codes are in the contract, not emitted yet. All reason codes
Updated · This page as Markdown · Changelog