ScamVerify™
Tutorials

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

What You Will Build

A /scan endpoint that:

  1. Accepts a URL from a form submission or API call
  2. Validates the URL format
  3. Calls the ScamVerify™ URL verification API
  4. Checks results against malware databases and threat feeds
  5. Displays a safety report to the user
  6. 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-dotenv

Create a .env file with your API key:

SCAMVERIFY_API_KEY=sv_live_your_key_here
FLASK_SECRET_KEY=your-secret-key-here

Create 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.py

Test 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 500

Test 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:

SignalWhat It Means
urlhaus_match: trueDomain found in the URLhaus malware database. Likely hosting malicious content.
threatfox_match: trueDomain found in the ThreatFox IOC database. Associated with known threat actors.
community_reports > 0Users have reported this URL as suspicious or malicious.
risk_score >= 70Multiple 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-dotenv

Low 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

On this page