Add URL Scanning to Your App
Step-by-step tutorial for adding URL scanning to a Python Flask application using the ScamVerify™ API.
This tutorial walks you through building a URL scanner in Python with Flask. By the end, you will have a working web endpoint that accepts a URL, checks it against the ScamVerify™ threat intelligence database (including URLhaus, ThreatFox, and community reports), and displays a clear safety verdict.
Prerequisites
- Python 3.9 or later
- A ScamVerify™ API key (get one at scamverify.ai/settings/api)
- Basic familiarity with Flask
What You Will Build
A /scan endpoint that:
- Accepts a URL from a form submission or API call
- Validates the URL format
- Calls the ScamVerify™ URL verification API
- Checks results against malware databases and threat feeds
- Displays a safety report to the user
- Handles errors and edge cases
Set up the project
Create a virtual environment and install dependencies.
mkdir url-scanner && cd url-scanner
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install flask requests python-dotenvCreate a .env file with your API key:
SCAMVERIFY_API_KEY=sv_live_your_key_here
FLASK_SECRET_KEY=your-secret-key-hereCreate the ScamVerify™ client
Build a Python client that handles API calls and error responses.
# scamverify.py
import requests
from dataclasses import dataclass
from typing import Optional
class ScamVerifyError(Exception):
def __init__(self, message: str, status_code: int = 0):
super().__init__(message)
self.status_code = status_code
@dataclass
class UrlScanResult:
risk_score: int
verdict: str
explanation: str
signals: dict
cached: bool
@property
def is_safe(self) -> bool:
return self.verdict in ("safe", "low_risk")
@property
def is_dangerous(self) -> bool:
return self.verdict in ("high_risk", "critical")
@property
def threat_feeds_matched(self) -> list[str]:
feeds = []
if self.signals.get("urlhaus_match"):
feeds.append("URLhaus")
if self.signals.get("threatfox_match"):
feeds.append("ThreatFox")
if self.signals.get("community_reports", 0) > 0:
feeds.append("Community Reports")
return feeds
class ScamVerifyClient:
BASE_URL = "https://scamverify.ai/api/v1"
def __init__(self, api_key: str):
if not api_key or not api_key.startswith("sv_"):
raise ValueError("Invalid API key. Keys must start with sv_live_ or sv_test_")
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
})
def lookup_url(self, url: str, force_refresh: bool = False) -> UrlScanResult:
payload = {"url": url}
if force_refresh:
payload["force_refresh"] = True
response = self.session.post(f"{self.BASE_URL}/url/lookup", json=payload)
if response.status_code == 401:
raise ScamVerifyError("Invalid or revoked API key", 401)
if response.status_code == 402:
raise ScamVerifyError("Quota exhausted. Upgrade your plan or wait for reset.", 402)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After", "60")
raise ScamVerifyError(f"Rate limited. Retry after {retry_after} seconds.", 429)
if not response.ok:
raise ScamVerifyError(f"API error: {response.status_code}", response.status_code)
data = response.json()
return UrlScanResult(
risk_score=data["risk_score"],
verdict=data["verdict"],
explanation=data["explanation"],
signals=data.get("signals", {}),
cached=data.get("cached", False),
)Build the Flask application
Create the main application with a scan endpoint and a simple HTML form.
# app.py
import os
from urllib.parse import urlparse
from flask import Flask, request, jsonify, render_template_string
from dotenv import load_dotenv
from scamverify import ScamVerifyClient, ScamVerifyError
load_dotenv()
app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-key")
client = ScamVerifyClient(os.environ["SCAMVERIFY_API_KEY"])
SCAN_TEMPLATE = """
<!DOCTYPE html>
<html>
<head><title>URL Scanner</title></head>
<body>
<h1>URL Safety Scanner</h1>
<form method="POST" action="/scan">
<input type="url" name="url" placeholder="https://example.com" required size="50" />
<button type="submit">Scan URL</button>
</form>
{% if result %}
<div style="margin-top: 20px; padding: 16px; border: 1px solid #ccc; border-radius: 8px;">
<h2>{{ result.verdict_label }} (Score: {{ result.risk_score }}/100)</h2>
<p>{{ result.explanation }}</p>
<ul>
<li>Domain: {{ result.domain }}</li>
{% if result.threat_feeds %}
<li>Matched threat feeds: {{ result.threat_feeds }}</li>
{% endif %}
<li>Cached: {{ "Yes" if result.cached else "No" }}</li>
</ul>
</div>
{% endif %}
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
</body>
</html>
"""
VERDICT_LABELS = {
"safe": "Safe",
"low_risk": "Low Risk",
"medium_risk": "Medium Risk",
"high_risk": "High Risk",
"critical": "Dangerous",
}
def validate_url(url: str) -> bool:
"""Check that the URL has a valid scheme and hostname."""
try:
parsed = urlparse(url)
return parsed.scheme in ("http", "https") and bool(parsed.hostname)
except Exception:
return False
@app.route("/", methods=["GET"])
def index():
return render_template_string(SCAN_TEMPLATE)
@app.route("/scan", methods=["GET", "POST"])
def scan():
if request.method == "GET":
return render_template_string(SCAN_TEMPLATE)
url = request.form.get("url") or (request.json or {}).get("url")
is_api_request = request.content_type == "application/json"
if not url:
error = "Please provide a URL to scan."
if is_api_request:
return jsonify({"error": error}), 400
return render_template_string(SCAN_TEMPLATE, error=error)
if not validate_url(url):
error = "Invalid URL format. Include http:// or https://"
if is_api_request:
return jsonify({"error": error}), 400
return render_template_string(SCAN_TEMPLATE, error=error)
try:
scan_result = client.lookup_url(url)
result_data = {
"risk_score": scan_result.risk_score,
"verdict": scan_result.verdict,
"verdict_label": VERDICT_LABELS.get(scan_result.verdict, "Unknown"),
"explanation": scan_result.explanation,
"domain": urlparse(url).hostname,
"threat_feeds": ", ".join(scan_result.threat_feeds_matched),
"cached": scan_result.cached,
"signals": scan_result.signals,
}
if is_api_request:
return jsonify({"success": True, "url": url, **result_data})
return render_template_string(SCAN_TEMPLATE, result=result_data)
except ScamVerifyError as e:
error = str(e)
if is_api_request:
return jsonify({"error": error}), e.status_code or 500
return render_template_string(SCAN_TEMPLATE, error=error)
except Exception as e:
error = "An unexpected error occurred. Please try again."
if is_api_request:
return jsonify({"error": error}), 500
return render_template_string(SCAN_TEMPLATE, error=error)
@app.route("/health")
def health():
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(debug=True, port=3000)Test the scanner
Start the Flask server and test with cURL or your browser.
python app.pyTest via cURL (JSON API mode):
# Scan a URL
curl -X POST http://localhost:3000/scan \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
# Test with a suspicious URL
curl -X POST http://localhost:3000/scan \
-H "Content-Type: application/json" \
-d '{"url": "https://suspicious-example.com/login"}'Or open http://localhost:3000 in your browser to use the HTML form.
Add batch scanning
For scanning multiple URLs at once (for example, all links found on a webpage), use the batch endpoint.
# Add to scamverify.py
def batch_lookup_urls(self, urls: list[str]) -> list[dict]:
"""Scan up to 100 URLs in a single request."""
if len(urls) > 50:
raise ValueError("Batch limit is 50 URLs per request")
response = self.session.post(
f"{self.BASE_URL}/batch/url",
json={"items": [{"url": u} for u in urls]},
)
if not response.ok:
raise ScamVerifyError(f"Batch API error: {response.status_code}", response.status_code)
data = response.json()
return data["results"]# Add to app.py
@app.route("/scan/batch", methods=["POST"])
def scan_batch():
urls = (request.json or {}).get("urls", [])
if not urls:
return jsonify({"error": "Provide a list of URLs in the 'urls' field"}), 400
if len(urls) > 50:
return jsonify({"error": "Maximum 50 URLs per batch request"}), 400
try:
results = client.batch_lookup_urls(urls)
return jsonify({"success": True, "results": results})
except ScamVerifyError as e:
return jsonify({"error": str(e)}), e.status_code or 500Test the batch endpoint:
curl -X POST http://localhost:3000/scan/batch \
-H "Content-Type: application/json" \
-d '{"urls": ["https://example.com", "https://another-example.com"]}'Interpret URL scan results
The URL verification API returns detailed signals about each scanned URL. Here is how to interpret the key fields:
| Signal | What It Means |
|---|---|
urlhaus_match: true | Domain found in the URLhaus malware database. Likely hosting malicious content. |
threatfox_match: true | Domain found in the ThreatFox IOC database. Associated with known threat actors. |
community_reports > 0 | Users have reported this URL as suspicious or malicious. |
risk_score >= 70 | Multiple threat signals detected. Treat this URL as dangerous. |
verdict: "safe" | No threats detected across any data source. |
Complete Project Structure
url-scanner/
.env # API key and Flask secret
scamverify.py # API client with URL lookup and batch methods
app.py # Flask application with /scan and /scan/batch endpoints
requirements.txt # flask, requests, python-dotenvLow confidence results. If a URL returns a low risk score with low confidence, it usually means the domain is new and has limited data across all sources. This does not mean the URL is safe. Treat low-confidence results as "unknown" rather than "safe."
Next Steps
- URL Verification API Reference for full request and response schemas
- E-commerce Fraud Prevention for using URL scanning in a marketplace
- Batch URL Lookup for processing large URL lists