# 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.

Updated 2026-10-30.

## At a glance

- 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?, https://bewary.app/check/link

## 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.

## Call it

curl:

```bash
curl "https://api.bewary.app/v1/url?u=http%3A%2F%2Fpaypal-login-secure.top%2Fverify%3Faccount%3Dupdate"
```

Python:

```python
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"])
```

JavaScript:

```javascript
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

Python:

```python
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
```

TypeScript, with the types from https://bewary.app/types/bewary.d.ts:

```typescript
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" }
}
```

## 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.

## Example response: scam

Request: `GET /v1/url?u=http://paypal-login-secure.top/verify?account=update`. Captured from the live API on 2026-10-31.

```json
{
  "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
}
```

## Example response: safe

Request: `GET /v1/url?u=https://www.usps.com/manage/`. Captured from the live API on 2026-10-31.

```json
{
  "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
}
```

## Link facts

Every key is in every response, null when unknown. Keys marked coming stay null until their source ships.

| 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 | In | Type | Required | Description |
|---|---|---|---|---|
| `u` | query | string | yes | The full URL, URL-encoded. Shorteners are followed. |
| `deep` | query | boolean | no | 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.

## Reason codes it can return

`LISTED_BLOCKLIST`, `DOMAIN_ALLOWLISTED`, `DOMAIN_AGE_GT_1Y`, `DOMAIN_AGE_90D_1Y`, `DOMAIN_AGE_30_90D`, `DOMAIN_AGE_LT_30D`, `DOMAIN_MULTITENANT_PLATFORM`, `DOMAIN_AGE_UNKNOWN`, `DOMAIN_LOOKALIKE_BRAND_WORD`, `DOMAIN_MIXED_SCRIPT`, `DOMAIN_HOMOGLYPH_BRAND`, `DOMAIN_BRAND_ON_ABUSED_TLD`, `DOMAIN_TYPO_BRAND`, `DOMAIN_BRAND_SCAM_WORD`, `TLD_HIGH_ABUSE`, `URL_LISTED_THREAT_INTEL`, `URL_HIGH_RISK_SCORE`, `URL_SUSPICIOUS_SCORE`, `PAGE_MALICIOUS`, `DOMAIN_DECEPTIVE_STRUCTURE`, `DOMAIN_CERT_LT_48H`, `URL_SHORTENER`, `URL_REDIRECT_CROSS_DOMAIN`, `LISTED_CAUTION`, `PROTECTIVE_DNS_BLOCKED`, `PLATFORM_TAKEDOWN`, `PLATFORM_WARNING`, `DOMAIN_REGISTRY_HOLD`, `DOMAIN_REGISTRAR_HOLD`, `SHORTLINK_DISABLED`, `SHORTLINK_WARNING`, `PAGE_CREDENTIAL_HARVEST`, `PAGE_PAYMENT_HARVEST`
