Documentation

Complete API reference, CLI commands, and MCP server setup.

On this page

Overview

Send a URL, get structured signals back. You decide what to do with them.

Unphurl analyses URLs across seven dimensions: redirect behaviour, brand impersonation, domain intelligence (age, registrar, expiration, status codes, nameservers via RDAP), SSL/TLS validity, parked domain detection, URL structural analysis (length, path depth, subdomain count, entropy), and DNS enrichment (MX records).

It returns raw signals, a configurable score with 23 weights, and a full breakdown of how that score was calculated. No verdicts, no "safe/unsafe" labels. Your AI agent sets its own threshold.

Base URL

https://api.unphurl.com/v1

Authentication

Every request to check and account endpoints requires an API key via bearer token.

Authorization: Bearer uph_your_key_here

API keys are issued on signup and shown once. Store them securely. If you lose yours, rotate it via /v1/keys/rotate.

Quick Start

Choose your path: terminal (CLI), AI tool (MCP), or direct API calls.

1. Sign up

From your terminal (CLI):

unphurl signup --email you@example.com --name "Sarah"

# With a company name
unphurl signup --email you@example.com --name "Sarah" --company "Acme Corp"

From Claude, ChatGPT, Cursor, or OpenClaw (MCP):

Just ask your AI: "Sign me up for Unphurl with my email you@example.com." The signup tool works without an API key.

Direct API call:

curl -X POST https://api.unphurl.com/v1/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","first_name":"Sarah"}'

Returns your API key (shown once, save it immediately). Check your email and click the verification link. Your key won't work until verified. Every new account gets 3 free pipeline check credits to test with real URLs.

Verification link expires in 24 hours. If it expires or you didn't receive it:

  • CLI: unphurl verify --resend --email you@example.com
  • AI tool: Ask: "Resend my Unphurl verification email for you@example.com"
  • API: POST /v1/verify/resend with {"email":"you@example.com"}

2. Try your free credits

From your terminal:

unphurl https://suspicious-domain.xyz

From your AI tool:

Ask: "Check this URL with Unphurl: https://suspicious-domain.xyz"

Direct API call:

curl "https://api.unphurl.com/v1/check?url=https://suspicious-domain.xyz" \
  -H "Authorization: Bearer uph_your_key_here"

Known domains (Google, GitHub, etc.) and cached domains are always free and don't consume credits. Your 3 free credits are for unknown domains that need the full pipeline.

3. Buy more pipeline checks

From your terminal:

# View packages
unphurl pricing

# Buy credits (opens Stripe checkout in your browser)
unphurl purchase pkg_100

From your AI tool:

Ask: "Buy me 100 Unphurl credits." Your AI will start the purchase and give you a link to complete payment.

Direct API call:

curl -X POST https://api.unphurl.com/v1/purchase \
  -H "Authorization: Bearer uph_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"package":"pkg_100"}'

Returns a Stripe Checkout URL. Open it in a browser to complete payment. Credits are added automatically. No subscription, credits never expire.

Check a URL

GET /v1/check?url={url}
Authorization: Bearer uph_your_key_here

Analyses a single URL. Returns signals, score, score breakdown, and metadata. Add ?profile=name to use a custom scoring profile.

Full pipeline response

{
  "url": "https://suspicious-domain.xyz/login",
  "final_url": "https://suspicious-domain.xyz/phish/page",
  "domain": "suspicious-domain.xyz",
  "score": 75,
  "score_breakdown": [
    { "signal": "brand_impersonation", "points": 40,
      "description": "impersonating paypal.com" },
    { "signal": "domain_age", "points": 35,
      "description": "domain registered 3 days ago" }
  ],
  "signals": {
    "url_analysis": {
      "url_length": 247,
      "path_depth": 3,
      "subdomain_count": 0,
      "domain_entropy": 3.1,
      "contains_ip": false,
      "encoded_hostname": false,
      "tld_changed_on_redirect": false
    },
    "redirects": {
      "chain": [
        "https://suspicious-domain.xyz/login",
        "https://suspicious-domain.xyz/redir",
        "https://suspicious-domain.xyz/phish/page"
      ],
      "count": 3,
      "initial_shortener": null,
      "stopped_reason": null
    },
    "domain": {
      "age_days": 3,
      "registrar": "NameCheap, Inc.",
      "expires_days": 362,
      "status_codes": ["clientTransferProhibited"],
      "nameservers": ["ns1.registrar-servers.com"],
      "has_mx_record": false,
      "is_parked": false,
      "is_known": false,
      "content_type": "text/html"
    },
    "ssl": { "valid": true },
    "phishing": {
      "is_phishing": true,
      "brand_impersonation": "paypal.com"
    }
  },
  "meta": {
    "checked_at": "2026-04-03T14:22:00Z",
    "latency_ms": 1240,
    "cached": false,
    "cache_age_hours": null,
    "pipeline_check_charged": true,
    "cache_ttl_remaining_hours": null
  }
}

Known domain response (free)

{
  "url": "https://google.com",
  "final_url": "https://www.google.com/",
  "domain": "google.com",
  "score": 0,
  "score_breakdown": [],
  "signals": {
    "domain": { "is_known": true, "is_parked": false },
    "ssl": { "valid": true },
    "phishing": { "is_phishing": false }
  },
  "meta": {
    "latency_ms": 45,
    "cached": false,
    "pipeline_check_charged": false
  }
}

Batch Check

POST /v1/check/batch
Authorization: Bearer uph_your_key_here
Content-Type: application/json

{
  "urls": [
    "https://google.com",
    "https://suspicious-domain.xyz",
    "https://bit.ly/abc123"
  ],
  "profile": "cold-email",
  "webhook_url": "https://your-server.com/results"
}
  • Maximum 500 URLs per request. The CLI auto-chunks larger files. The MCP server handles polling automatically.
  • Duplicates are silently deduplicated (process once, charge once).
  • Invalid URLs get per-URL status: "error" without rejecting the batch.
  • Known/cached URLs resolve immediately. Unknowns are queued for async processing.
  • Optional: profile for custom scoring, webhook_url for delivery.

Response (with pending URLs)

{
  "results": [
    { "url": "https://google.com",
      "status": "complete",
      "result": { "...full response..." } },
    { "url": "https://suspicious-domain.xyz",
      "status": "pending" },
    { "url": "https://bit.ly/abc123",
      "status": "pending" }
  ],
  "summary": {
    "total": 3,
    "unique": 3,
    "duplicates_removed": 0,
    "complete": 1,
    "pending": 2,
    "errors": 0,
    "pipeline_checks_queued": 2
  },
  "job_id": "abc123-uuid-here",
  "poll_url": "/v1/jobs/abc123-uuid-here"
}

Job Polling

GET /v1/jobs/{job_id}
Authorization: Bearer uph_your_key_here

Poll for batch results. Job statuses: processing (still running) or completed (all done). Jobs expire after 24 hours.

{
  "job_id": "abc123-uuid-here",
  "status": "completed",
  "results": [
    { "url": "https://suspicious-domain.xyz",
      "status": "completed",
      "result": { "...full response..." } }
  ],
  "summary": {
    "total": 2,
    "completed": 2,
    "pending": 0,
    "failed": 0,
    "pipeline_checks_charged": 2
  }
}

pipeline_checks_charged counts only successful completions. Failed pipelines are not charged.

Webhooks

Include a webhook_url in your batch request. When the job completes, Unphurl POSTs the results to your URL. Same payload format as job polling.

Every webhook includes an X-Unphurl-Signature header (HMAC-SHA256 of the body, signed with your API key hash) for verification. 3 retries with exponential backoff (1s, 5s, 25s). Results are always available via polling too.

Filtering Results

After a batch check, you'll want to split results into clean and flagged lists. Two paths depending on your workflow.

CLI (developers)

# Get only clean URLs (score under 25)
unphurl --batch urls.txt --json | \
  jq -r '.results[] | select(.result.score < 25) | .url' > clean-urls.txt

# Get flagged URLs (score 50+)
unphurl --batch urls.txt --json | \
  jq -r '.results[] | select(.result.score >= 50) | .url' > flagged-urls.txt

# Export full results as CSV
unphurl --batch urls.txt --json | \
  jq -r '.results[] | [.url, .result.score, .result.domain, .result.signals.phishing.is_phishing] | @csv' > results.csv

Claude Cowork / MCP (non-coders)

Your AI is the filter. Give it the batch and tell it what you want:

"Check these 500 URLs. Give me two lists: the clean ones (score under 25) and the flagged ones (score 50 or higher). Export both as CSV."

Claude gets the batch results via MCP, filters them, and outputs the lists. No jq, no code. This is one of the moments where the non-coder experience is actually better than the developer experience.

Known Domain Check

GET /v1/known?domain=google.com

No authentication required. Returns whether a domain is in the Tranco Top 100K. Useful for predicting whether a check will be free.

{
  "domain": "google.com",
  "is_known": true,
  "source": "tranco_top_100k"
}

Account

Sign up

POST /v1/signup
{ "email": "you@example.com", "first_name": "Sarah" }

Returns API key (shown once). Sends verification email. Key doesn't work until verified.

Resend verification email

POST /v1/verify/resend
{ "email": "you@example.com" }

No authentication required. Rate limited to 3 per email per hour. Response is always the same regardless of whether the account exists:

{ "message": "If an account with that email exists and is not yet verified, a new verification link has been sent. Check your inbox." }

Account info

GET /v1/account    →    { "email": "...", "verified": true, "balance": 487 }

Rotate API key

POST /v1/keys/rotate

New key generated, old key revoked immediately. Balance and history preserved.

Delete account

DELETE /v1/account

Hard delete. Email, key, balance, history erased immediately. Irreversible.

Check history

GET /v1/history?page=1&limit=20

Paginated log of your checks. Domain, score, phishing status, timestamp. 90-day retention.

Usage statistics

GET /v1/account/stats

Authenticated. Returns usage analytics for your account.

{
  "usage": {
    "total_urls_submitted": 12450,
    "tranco_lookups": 8200,
    "cache_lookups": 3890,
    "pipeline_checks_run": 360,
    "free_rate_pct": 97.1
  },
  "scoring": {
    "checks_above_50": 142,
    "checks_above_75": 38
  },
  "account": {
    "credits_remaining": 145,
    "total_credits_purchased": 500,
    "last_active_at": "2026-04-08T14:30:00Z"
  }
}

usage.total_urls_submitted — total URLs you've checked (all gates)

usage.tranco_lookups — URLs that matched the Tranco Top 100K (free)

usage.cache_lookups — URLs that matched the reputation cache (free)

usage.pipeline_checks_run — URLs that ran through the full pipeline (paid)

usage.free_rate_pct — percentage of lookups that were free

scoring.checks_above_50 — URLs that scored above 50

scoring.checks_above_75 — URLs that scored above 75

account.credits_remaining — current credit balance

account.total_credits_purchased — lifetime credits purchased

account.last_active_at — last API activity timestamp

Billing

Check balance

GET /v1/balance

{
  "credits_remaining": 487,
  "total_purchased": 500,
  "total_used": 13,
  "free_lookups": 1042
}

Purchase credits

POST /v1/purchase
{ "package": "pkg_500" }
Package Credits Price Per credit
pkg_100100$9$0.090
pkg_500500$39$0.078
pkg_20002,000$99$0.050
pkg_1000010,000$399$0.040

View pricing

GET /v1/pricing

No authentication required. Returns all packages and billing info.

Billing Behaviour

Positive balance required

All check requests require at least 1 credit on your account, even for free lookups. Known domains (Tranco) and cached domains don't deduct credits, but a positive balance is required to make the request. At 0 credits, you get a 402 response with a summary of what the URL would have cost.

Single check at 0 credits

Returns 402 with a smart summary: whether the URL is known, cached, or unknown, whether a pipeline check would be needed, and how many credits to purchase.

Batch is all-or-nothing

If you don't have enough credits for every unknown URL in the batch, none of them process. You still get a smart summary showing how many URLs are known, cached, and unknown, and exactly how many credits you need.

{
  "error": "insufficient_credits",
  "summary": {
    "total": 100,
    "tranco": 20,
    "cached": 30,
    "unknown": 50,
    "credits_needed": 50,
    "credits_remaining": 3
  },
  "message": "This batch has 50 unknown domains requiring pipeline checks. You have 3 credits. Purchase 47 more to run this batch.",
  "purchase_url": "/v1/purchase"
}

Failed pipeline = refund

If the pipeline fails completely and returns zero signals, the credit is automatically refunded. For batch jobs, each failed URL is refunded individually.

Partial success = charged

If some signals succeed but others time out (e.g., RDAP times out but SSL check works), the result is returned with null values for the failed checks. The credit is charged because you received intelligence.

Scoring Profiles

Different use cases care about different signals. Profiles are sparse overrides merged with defaults. Up to 20 per account.

Create a profile

POST /v1/profiles
{
  "name": "cold-email",
  "weights": {
    "parked": 30,
    "chain_incomplete": 25,
    "no_mx_record": 20,
    "expiring_soon": 15,
    "ssl_invalid": 15,
    "http_only": 15,
    "brand_impersonation": 10,
    "domain_age_7": 5
  }
}

Use a profile

GET /v1/check?url=https://example.com&profile=cold-email

List profiles

GET /v1/profiles

Delete a profile

DELETE /v1/profiles/cold-email

Example profiles by use case

Slack/Teams Security Bot
brand_impersonation: 40, domain_age_3: 40, domain_age_7: 30, redirects_5: 25, url_contains_ip: 15, ssl_invalid: 15, phishing_floor: 80
Cold Email Outreach
parked: 30, chain_incomplete: 25, no_mx_record: 20, expiring_soon: 15, ssl_invalid: 15, http_only: 15, brand_impersonation: 10
Lead Gen Validation
parked: 35, http_only: 20, chain_incomplete: 20, domain_status_bad: 20, no_mx_record: 15, expiring_soon: 15, ssl_invalid: 15

Scoring Weights

Default weights used when no profile is specified. Every weight can be overridden per profile.

Signal Key Default Detects
Brand impersonation brand_impersonation +40 Domain resembles a major brand
Domain age <=3 days domain_age_3 +35 Peak phishing window
Domain age <7 days domain_age_7 +25 Registered in last week
Domain age <30 days domain_age_30 +15 Registered in last month
Domain age <90 days domain_age_90 +5 Registered in last 3 months
SSL invalid ssl_invalid +10 Certificate missing, expired, or invalid
HTTP only http_only +5 No SSL at all
3-4 redirects redirects_3 +10 Moderate redirect chain
5+ redirects redirects_5 +25 Excessive redirects
Chain incomplete chain_incomplete +15 Could not follow full redirect chain
Parked domain parked +10 Registrar placeholder or parking service
URL length >200 url_long +3 Unusually long URL
Path depth >4 path_deep +3 Deep path structure
Subdomain count >3 subdomain_excessive +5 Excessive subdomains
High entropy domain_entropy_high +5 Random-looking domain name
URL contains IP url_contains_ip +10 IP address instead of domain
Encoded hostname encoded_hostname +5 Percent-encoded characters in hostname
TLD changed tld_redirect_change +5 TLD differs after redirect
Expiring soon expiring_soon +10 Domain expires within 30 days
Bad status domain_status_bad +15 pendingDelete, serverHold, etc.
No MX record no_mx_record +5 Domain doesn't receive email
Compound compound +10 3+ signals detected together
Phishing floor phishing_floor 80 Min score when brand impersonation + other signal

How scores are calculated

  1. Pipeline gathers raw signals across 7 dimensions
  2. Each signal is checked against the weight table (default or your profile)
  3. Domain age uses 4 mutually exclusive brackets (only the most specific fires)
  4. Matching signals add their weight to the score
  5. If 3+ signals fire, the compound amplifier adds its weight
  6. If brand impersonation + any other signal, the phishing floor is enforced
  7. score_breakdown in the response shows every contributing signal

Errors

Every error returns structured JSON with an error code and a human-readable message.

Code HTTP Description
missing_url 400 No url parameter provided
invalid_url 400 URL is malformed or targets a private IP
profile_not_found 400 Specified profile name does not exist
invalid_name 400 Profile name does not match format rules
invalid_weights 400 Unknown weight keys or invalid values
profile_limit_reached 400 Account already has 20 profiles
invalid_api_key 401 API key missing, malformed, or invalid
insufficient_credits 402 No pipeline checks remaining
email_not_verified 403 Email not yet verified
rate_limited 429 Too many requests (check Retry-After header)
{
  "error": "insufficient_credits",
  "message": "No pipeline checks remaining. Purchase more to continue.",
  "credits_remaining": 0,
  "purchase_url": "/v1/purchase"
}

Rate Limits

Per API key. Designed to prevent bugs from burning credits, not to restrict normal use.

Endpoint Limit Notes
GET /v1/check60/minSingle URL checks
POST /v1/check/batch10/minUp to 500 URLs each = 5,000 URLs/min
GET /v1/known120/minLightweight lookup, no auth
POST /v1/signup5/hourPer IP
POST /v1/purchase10/hourPrevents duplicate purchases
All other60/minAccount, balance, history, profiles, jobs

Rate limit headers in every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. When limited, you get a 429 with a Retry-After header.

Comparison

Unphurl provides structured signals with configurable scoring. Unlike blocklist-based tools (Google Safe Browsing, VirusTotal), Unphurl analyses domain infrastructure in real time and catches threats too new for any database. Unlike enterprise tools (DomainTools, SecurityTrails), Unphurl requires no subscription and starts at $9. Unphurl also covers both security and data quality use cases, which none of the alternatives address with a single product.

See the full comparison chart on the homepage.

Limitations

  • No safety guarantees. Heuristic detection catches patterns, not zero-day exploits or novel attacks.
  • No page content scanning. Unphurl analyses the URL, domain, redirects, and TLS certificate. It does not load or render pages.
  • No verdicts. No "block" or "allow" recommendations. Signals and scores only. Your AI agent decides.
  • No third-party threat databases. Own heuristic analysis only. Catches pattern-based threats too new for databases.
  • Tranco Top 100K trusted. Known domains return score 0 without pipeline. If compromised, Unphurl will not catch it.
  • RDAP data can be incomplete. Some registries don't support RDAP. Domain age returns null when unavailable.

CLI Quick Start

Check URLs from the terminal. Colour-coded scores, JSON output, batch processing, full account management.

Check a URL

npx unphurl https://suspicious-domain.xyz

Batch check from a file

unphurl --batch urls.txt --profile cold-email

JSON output for piping

unphurl https://example.com --json | jq '.score'

Account management

# Sign up
unphurl signup --email you@example.com --name "Your Name"

# Resend verification email
unphurl verify --resend --email you@example.com

# Check balance
unphurl --balance

# View usage statistics
unphurl stats

# Buy credits
unphurl purchase pkg_500

# Manage profiles
unphurl profiles create cold-email --weights '{"parked":30,"ssl_invalid":15}'
unphurl profiles

Authentication

export UNPHURL_API_KEY=uph_your_key_here

Or pass --key uph_your_key_here per command.

MCP Quick Start

Native integration with Claude (Code, Desktop, or Cowork), ChatGPT desktop, Cursor, Windsurf, and any MCP-compatible AI tool. 13 tools with full API parity.

1. Add to your MCP configuration

Add to .mcp.json:

{
  "mcpServers": {
    "unphurl": {
      "command": "npx",
      "args": ["-y", "@unphurl/mcp-server"],
      "env": {
        "UNPHURL_API_KEY": "uph_your_key_here"
      }
    }
  }
}

2. Available tools

Tool Description Auth
signup Create a new account No
check_url Check a single URL Yes
check_urls Batch check up to 500 URLs Yes
list_profiles List scoring profiles Yes
create_profile Create or update a profile Yes
delete_profile Delete a profile Yes
show_defaults Show all scoring signals No
get_balance Check credit balance Yes
get_pricing Show packages and pricing No
purchase Purchase credits (Stripe URL) Yes
check_history View check history Yes
get_stats View usage statistics Yes
resend_verification Resend email verification link No

3. Companion skill (optional)

Teaches your AI to proactively check URLs before following or recommending them, without being asked.

Claude Cowork or Claude Desktop: Ask Claude: "Install the Unphurl URL safety skill." Claude will set it up for you.

Claude Code, Cursor, or other developer tools:

cp node_modules/@unphurl/mcp-server/skills/check-url-safety.md ~/.claude/skills/