Domain age and lookalike domain API
Bewary's domain API returns registration and expiry dates and registrar from RDAP, plus age in days. It checks scam list membership, how often its TLD is used for scams and brand imitation by homoglyph, typo, brand plus extra words or brand plus a scam word. The answer includes `verdict`, `reason_codes` and `site.p_scam_site`, Bewary's verdict on a page its worker read in the last 90 days. We measured first-time domain lookups from Europe at a median of 449 ms on October 23, 2026. 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 449 ms.
- Endpoint
- GET
https://api.bewary.app/v1/domain/{host} - Price
- 1 credit, deep=true 30
- Free key
- 1,000 credits a month, no card
- Speed
- Median 449 ms, measured
- Returns
- verdict, p_scam, entities with typed facts, reason codes
- MCP tool
lookup_domain- Free tool
- Domain age checker
Try it
Free from this page: 10 lookups a dayCall it
curl "https://api.bewary.app/v1/domain/paypal-login-secure.top" import requests
r = requests.get("https://api.bewary.app/v1/domain/paypal-login-secure.top", timeout=10)
result = r.json()
print(result["verdict"], result["p_scam"], result["reason_codes"]) const res = await fetch("https://api.bewary.app/v1/domain/paypal-login-secure.top")
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 domain_risk(host):
key = os.environ["BEWARY_API_KEY"] # a free key: bewary.app/account
headers = {"Authorization": f"Bearer {key}"}
r = requests.get(f"https://api.bewary.app/v1/domain/{host}", headers=headers, timeout=10)
r.raise_for_status()
result = r.json()
facts = result["entities"][0]["facts"]
age = facts["age_days"] # None when the registry has no date
return {"verdict": result["verdict"], "new_domain": None if age is None else age < 30,
"imitates": (facts["lookalike"] or {}).get("brand_id")}
print(domain_risk("paypal-login-secure.top"))
# {'verdict': 'scam', 'new_domain': None, 'imitates': 'paypal'} import type { Envelope } from "./bewary"
export async function domainRisk(host: string) {
const key = process.env.BEWARY_API_KEY! // a free key: bewary.app/account
const headers: Record<string, string> = { Authorization: `Bearer ${key}` }
const res = await fetch(`https://api.bewary.app/v1/domain/${encodeURIComponent(host)}`,
{ headers, signal: AbortSignal.timeout(10_000) })
if (!res.ok) throw new Error(`Bewary ${res.status}`)
const r: Envelope = await res.json()
const e = r.entities[0]
if (e.type !== "domain") throw new Error("unexpected entity") // narrows e.facts to DomainFacts
const age = e.facts.age_days // null when the registry has no date
return { verdict: r.verdict, newDomain: age === null ? null : age < 30, imitates: e.facts.lookalike?.brand_id ?? null }
} Response
The fields you will use
verdict- The domain's verdict.
entities[0].facts.age_days- Days since registration. registered_at has the date.
entities[0].facts.lookalike- The brand it imitates and the technique.
entities[0].facts.tld_abuse- How often its TLD is used for scams: low, medium or high.
entities[0].facts.listed- On Bewary's scam list.
{ "schema_version": "1.7.0", "request_id": "req_01M69WH7A2GYM3D5XTWA82PZ5B", "checked_at": "2026-10-31T21:06:00Z", "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": "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": "e1", "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": "e1", "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_01M69WH9P6H5FWFNDK0RSH7VWK", "checked_at": "2026-10-31T21:06:03Z", "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": "domain", "value": "chase.com", "normalized": "chase.com", "role": "link_target", "verdict": "safe", "p_scam": 0, "risk_score": 0, "reason_codes": [ "DOMAIN_ALLOWLISTED" ], "facts": { "registrable_domain": "chase.com", "registered_domain": "chase.com", "tld": "com", "tld_abuse": "low", "registered_at": "1995-10-11T04:00:00Z", "expires_at": "2027-10-10T04:00:00Z", "age_days": 11342, "age_bucket": "gt_1y", "registrar": "MarkMonitor Inc.", "dns": { "resolves": true, "a": [ "146.143.13.57", "146.143.141.57", "146.143.83.57" ], "mx": null, "ns": [ "ns0119.secondary.cloudflare.com", "ns0140.secondary.cloudflare.com", "ns05.jpmorganchase.com", "ns06.jpmorganchase.com", "ns1.jpmorganchase.com", "ns2.jpmorganchase.com" ] }, "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_delete_prohibited", "client_transfer_prohibited", "client_update_prohibited", "server_delete_prohibited", "server_transfer_prohibited", "server_update_prohibited" ], "hold": "none", "registered": true, "registration_checked_at": "2026-10-30T19:19:01Z", "archive": { "checked_at": "2026-10-30T19:20:21Z", "captured": true, "first_capture_at": "1996-11-15T03:41:13Z", "last_capture_at": "2026-10-30T19:18:26Z", "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": "e1", "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.
Domain 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 |
|---|---|---|
registrable_domain | string or null | The owner boundary per the Public Suffix List with its private section: x.pages.dev stays whole. Block by this. |
registered_domain | string or null | The name a registry sold: pages.dev for x.pages.dev. The registration dates and registrar describe this name. |
status | live | gone | broken | parked | held | unknown or null | What the domain is doing now. held: its registry or registrar holds it out of the DNS. |
tld | string or null | The top-level domain. |
tld_abuse | low | medium | high or null | How often this TLD is used for scams. |
registered_at | timestamp or null | Registration date from the public registry (RDAP). |
expires_at | timestamp or null | When the registration expires unless it is renewed (RDAP). |
age_days | integer or null | Days since registration. |
age_bucket | lt_7d | d7_30 | d30_90 | d90_365 | gt_1y | unknown or null | The age as a band, for simple rules. |
lookalike | object or null | The brand it imitates (brand_id) and the technique: homoglyph, typo, brand_word or brand_scam_word. subdomain, tld_swap and userinfo are reserved. |
platform | none | multi_tenant or null | multi_tenant: a site anyone can publish on, so the domain's age vouches for nothing. |
allowlisted | boolean or null | A well-known legitimate domain. |
listed | boolean or null | On Bewary's scam list. |
registrar | string or null | The registrar's name, from RDAP. |
registry_status | array or null | The registry's EPP status codes for registered_domain, such as server_hold or pending_delete. |
hold | none | registrar | registry | both or null | Who holds the name out of the DNS. A hold is how registries and registrars take a domain down. |
registered | boolean or null | false: the registry says no such name is registered, so anyone may register it. |
registration_checked_at | timestamp or null | When the registration record was read. Names on our lists are read again daily. |
dns | object or null | Whether it resolves, its IPs, MX and name servers. |
site | object or null | Our verdict on the site when our worker read its page in the last 90 days, with a calibrated probability (p_scam_site). |
platform_status | object or null | The hosting platform's own answer about the site, such as a takedown page or a phishing warning. |
archive | object or null | The site's history in a public web archive, including captures from before the current registration. |
first_seen_by_bewary | timestamp or null | When our own watch first saw the host, in a new certificate or a registry zone file. |
first_listed_publicly | timestamp or null | When a public list we read first named the host. |
country | ISO code or null | The country its country-code TLD names. null for generic TLDs. |
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'. |
cert_first_seenComing | timestamp or null | When its first TLS certificate was logged. |
seen_in_checks_7dComing | integer or null | How often it appeared in checks this week. |
Parameters
| Name | Type | Description |
|---|---|---|
hostpath, required | string | The domain or hostname, e.g. paypal-login-secure.top. Subdomains are fine. |
deepquery | boolean | Also read the page with a model (30 credits, API key required). Default false. |
Questions
How do I get a domain's age with an API?
GET /v1/domain/{host}. facts.registered_at, facts.age_days, facts.expires_at and facts.registrar come from the public registration data (RDAP), and age_bucket gives a band such as lt_7d or gt_1y for simple rules. The free domain age checker at bewary.app/check/domain-age makes the same call.
Can it detect lookalike and typosquatting domains?
Yes, for well-known brands. facts.lookalike names the brand and the technique: homoglyph (paypa1.com), typo, brand_word (paypal-login.top) or brand_scam_word. Subdomain, TLD swap and userinfo tricks are reserved values, added to the contract and not reported yet.
Is a new domain a scam?
Not on its own. A domain under 30 days old raises the score, but alone it never makes a scam verdict, because real businesses launch new sites too. It needs a second signal, such as a lookalike brand name.
How fast is it?
Measured median 449 ms for a domain seen for the first time.
Used for
- Sign-up and checkout checks on the email or website a user gives you.
- Brand protection: spotting lookalike domains that use your name.
- Flagging newly registered domains in a browser extension or security alert.
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 |
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 |
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 |
Reserved codes are in the contract, not emitted yet. All reason codes
Updated · This page as Markdown · Changelog