Documentation

URLScanner API

Programmatically scan URLs for malware, phishing, and threats. Integrate URL scanning directly into your security pipelines, SOAR playbooks, or custom tooling.

API keys available on all plans

Free (10/day) and Solo (100/day) can use the sync endpoint. Team (500/day) and Business (2,000/day) unlock the async endpoint too.

Get API key →

Overview

The URLScanner API is a REST API. All requests are made to https://urlscanner.online/api and return JSON.

Scanning is asynchronous: POST /api/scan starts the scan and returns a scanId. Poll GET /api/scan/{scanId} until status is completed — typically 5–15 seconds. If the URL was recently scanned a cached result is returned immediately and does not count against your quota.

Step 1
Submit URL
POST /api/scan
Step 2
Receive scanId
or instant cached result
Step 3
Poll for result
GET /api/scan/{id}

Authentication

All API requests require an API key passed in the X-API-Key header. Generate and manage your keys from the Aprensec dashboard or from your account settings on this site (avatar → API Keys).

http
POST /api/scan HTTP/1.1
Host: urlscanner.online
X-API-Key: usc_your_api_key_here
Content-Type: application/json

{"url": "https://example.com"}

Keep your key secret

Never expose it in client-side code, browser extensions, or public repositories. If a key is compromised, revoke it immediately from the dashboard and generate a new one.

Endpoints

MethodPathAuthDescription
POST/api/scanRequiredSubmit URL — async, returns a scanId to poll
GET/api/scan/{scanId}NonePoll for completed async scan result
POST/api/scan/syncRequiredSynchronous — blocks and returns full JSON in one request. Best for Zapier, Make, scripts.

POST /api/scan — Submit a URL

Starts a new scan and returns immediately. Cached results (URLs scanned within the last 7 days) are returned inline without consuming quota.

Request body

FieldTypeRequiredDescription
urlstringYesURL, domain, or IP address to scan
rescanbooleanNoForce a fresh scan, bypassing the 7-day cache

Response — fresh scan

json
{
  "scanId":        "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "url":           "https://example.com",
  "normalizedUrl": "https://example.com/",
  "hostname":      "example.com",
  "remaining":     499,
  "plan":          "team"
}

Response — cached result

json
{
  "cached":       true,
  "cachedAt":     "2026-03-11T10:00:00.000Z",
  "cachedResult": { "status": "completed", "score": 0, "verdict": "clean", ... },
  "plan":         "team"
}

GET /api/scan/{scanId} — Retrieve Result

Poll this endpoint until status is "completed". We recommend polling every 2 seconds with a 60-second timeout. Results are held in memory for up to 10 minutes after completion.

Response fields

FieldTypeDescription
statusstring"running" | "completed" | "failed"
scorenumberThreat score 0–100 (higher = more dangerous)
verdictstring"clean" | "low" | "medium" | "high" | "critical"
dnsobject | nullDNS records: A, AAAA, MX, TXT, NS, SOA, CAA
sslobject | nullTLS certificate chain, protocol version, cipher
httpobject | nullHTTP response headers and security header analysis
whoisobject | nullDomain registration, registrar, age, expiry
threatsobject | nullURLhaus, Spamhaus, SURBL blocklist results
screenshotsarray | nullScreenshot URLs at desktop / mobile / tablet viewports
aiAnalysisobject | nullAI-generated risk summary, confidence, key findings
scanDurationMsnumber | nullTotal scan time in milliseconds

POST /api/scan/sync — Synchronous Scan

Built for Zapier, Make, and scripts

This endpoint blocks until the scan is complete and returns the full result in a single HTTP response — no polling required. Modules run in parallel and results are returned as soon as all finish (or their timeout is hit). Typical response time is 15–20 seconds.

ModuleIncludedTimeout
DNS8s
HTTP Headers10s
WHOIS8s
SSL8s
Threat checks12s
AI analysis~22s combined
Screenshots✗ skipped

If a module times out it returns null for that field — the response is always returned, never blocked indefinitely. Screenshots are excluded to keep response times predictable.

Request body

Same as POST /api/scan — accepts url and optional rescan.

http
POST /api/scan/sync HTTP/1.1
Host: urlscanner.online
X-API-Key: usc_your_api_key_here
Content-Type: application/json

{"url": "https://example.com"}

Response

json
{
  "cached":        false,
  "url":           "https://example.com/",
  "hostname":      "example.com",
  "score":         85,
  "verdict":       "safe",
  "scannedAt":     "2026-03-11T12:00:00.000Z",
  "scanDurationMs": 14320,
  "remaining":     498,
  "dns":     { "resolvedIp": "93.184.216.34", "records": [...] },
  "ssl":     { "valid": true, "issuer": "DigiCert", "daysUntilExpiry": 180, ... },
  "http":    { "statusCode": 200, "securityHeaders": [...], ... },
  "whois":   { "domainAge": 10957, "registrar": "IANA", ... },
  "threats": { "urlhaus": { "flagged": false }, "dnsBlocklists": { "spamhaus": false, "surbl": false }, ... },
  "aiAnalysis": {
    "knownDomain":    true,
    "domainReputation": "trusted",
    "domainCategory": "Example / documentation domain",
    "score":          90,
    "riskLevel":      "low",
    "summary":        "example.com is the IANA reserved example domain...",
    "briefSummary":   "Clean, well-known IANA domain with no threats detected.",
    "recommendations": []
  }
}

Zapier / Make — quick setup

Use a Webhooks by Zapier or Make HTTP module action with:

MethodPOST
URLhttps://urlscanner.online/api/scan/sync
HeaderX-API-Key: usc_your_key
Content-Typeapplication/json
Body{"url": "{{url_variable}}"}

Map output fields like verdict, score, aiAnalysis.briefSummary, and threats.urlhaus.flagged directly to subsequent steps in your automation.

Rate Limits

API quotas are tracked separately from web UI scans and reset at midnight UTC. Cached results are free and do not count.

PlanAPI scans / dayPrice
Free10 / day (sync only)€0
Solo100 / day (sync only)€29 / mo
Team500 / day€99 / mo
Business2,000 / day€299 / mo

When the limit is reached the API returns 429 Too Many Requests with a resetAt field (Unix milliseconds) indicating when the quota resets.

Error Responses

All errors return JSON with an error string. HTTP status codes follow standard conventions.

StatusErrorMeaning
400Invalid URL / blockedURL is malformed, private, or on an SSRF blocklist
401Invalid API keyX-API-Key header is missing or the key is revoked
403Plan requiredYour current plan does not include API access
429Daily limit reachedQuota exhausted — check resetAt to know when it refills
500Internal server errorUnexpected server-side failure

Example error body

json
{
  "error":   "Daily API limit reached (500/day)",
  "resetAt": 1741737600000
}

Code Examples

All examples follow the same pattern: submit → check for cache hit → poll until complete. Replace usc_your_api_key_here with your key from the Aprensec dashboard.

cURL

bash
API_KEY="usc_your_api_key_here"

# 1. Submit a scan
RESPONSE=$(curl -s -X POST https://urlscanner.online/api/scan \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}')

echo $RESPONSE

# 2. Extract scanId and poll (requires jq)
SCAN_ID=$(echo $RESPONSE | jq -r '.scanId')

while true; do
  sleep 2
  RESULT=$(curl -s "https://urlscanner.online/api/scan/$SCAN_ID")
  STATUS=$(echo $RESULT | jq -r '.status')
  if [ "$STATUS" = "completed" ]; then
    echo "Verdict: $(echo $RESULT | jq -r '.verdict')"
    echo "Score:   $(echo $RESULT | jq -r '.score')"
    break
  fi
done

JavaScript / TypeScript

typescript
const API_KEY = "usc_your_api_key_here";
const BASE    = "https://urlscanner.online/api";

async function scanUrl(url: string, timeoutMs = 60_000) {
  // 1. Submit
  const res = await fetch(`${BASE}/scan`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ url }),
  });

  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`[${res.status}] ${error}`);
  }

  const data = await res.json();

  // Cached — returned immediately, free of charge
  if (data.cached) return data.cachedResult;

  // 2. Poll until completed
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, 2000));

    const poll = await fetch(`${BASE}/scan/${data.scanId}`);
    const result = await poll.json();

    if (result.status === "completed") return result;
    if (result.status === "failed")    throw new Error("Scan failed");
  }

  throw new Error("Scan timed out");
}

// Usage
const result = await scanUrl("https://example.com");
console.log(result.verdict, result.score);
// e.g. "clean" 0

Python

python
import time
import requests

API_KEY = "usc_your_api_key_here"
BASE    = "https://urlscanner.online/api"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

def scan_url(url: str, timeout: int = 60) -> dict:
    # 1. Submit
    r = requests.post(f"{BASE}/scan", json={"url": url}, headers=HEADERS)
    r.raise_for_status()
    data = r.json()

    # Cached — returned immediately, free of charge
    if data.get("cached"):
        return data["cachedResult"]

    scan_id = data["scanId"]
    deadline = time.time() + timeout

    # 2. Poll until completed
    while time.time() < deadline:
        time.sleep(2)
        r = requests.get(f"{BASE}/scan/{scan_id}")
        r.raise_for_status()
        result = r.json()

        if result["status"] == "completed":
            return result
        if result["status"] == "failed":
            raise RuntimeError("Scan failed")

    raise TimeoutError(f"Scan did not complete within {timeout}s")

# Usage
result = scan_url("https://example.com")
print(result["verdict"], result["score"])
# e.g. clean 0

# Bulk scanning
urls = ["https://example.com", "https://github.com", "https://google.com"]
for url in urls:
    r = scan_url(url)
    print(f"{url:40s}  {r['verdict']:8s}  score={r['score']}")

Go

go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

const (
    apiKey = "usc_your_api_key_here"
    base   = "https://urlscanner.online/api"
)

type submitResp struct {
    ScanID       string          `json:"scanId"`
    Cached       bool            `json:"cached"`
    CachedResult json.RawMessage `json:"cachedResult"`
}

type resultResp struct {
    Status  string `json:"status"`
    Score   int    `json:"score"`
    Verdict string `json:"verdict"`
}

func scanURL(target string) (*resultResp, error) {
    client := &http.Client{Timeout: 10 * time.Second}

    // 1. Submit
    body, _ := json.Marshal(map[string]string{"url": target})
    req, _ := http.NewRequest("POST", base+"/scan", bytes.NewReader(body))
    req.Header.Set("X-API-Key", apiKey)
    req.Header.Set("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var sub submitResp
    json.NewDecoder(resp.Body).Decode(&sub)

    // Cached — free of charge
    if sub.Cached {
        var r resultResp
        json.Unmarshal(sub.CachedResult, &r)
        return &r, nil
    }

    // 2. Poll until completed
    deadline := time.Now().Add(60 * time.Second)
    for time.Now().Before(deadline) {
        time.Sleep(2 * time.Second)

        r, _ := http.NewRequest("GET", base+"/scan/"+sub.ScanID, nil)
        res, err := client.Do(r)
        if err != nil {
            continue
        }

        var result resultResp
        json.NewDecoder(res.Body).Decode(&result)
        res.Body.Close()

        if result.Status == "completed" {
            return &result, nil
        }
    }
    return nil, fmt.Errorf("scan timed out")
}

func main() {
    result, err := scanURL("https://example.com")
    if err != nil {
        panic(err)
    }
    fmt.Printf("verdict=%s score=%d\n", result.Verdict, result.Score)
}

PHP

php
<?php

define('API_KEY', 'usc_your_api_key_here');
define('BASE',    'https://urlscanner.online/api');

function scan_url(string $url, int $timeout = 60): array {
    // 1. Submit
    $ch = curl_init(BASE . '/scan');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => [
            'X-API-Key: ' . API_KEY,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode(['url' => $url]),
    ]);
    $data = json_decode(curl_exec($ch), true);
    curl_close($ch);

    // Cached — free of charge
    if (!empty($data['cached'])) {
        return $data['cachedResult'];
    }

    $scanId   = $data['scanId'];
    $deadline = time() + $timeout;

    // 2. Poll until completed
    while (time() < $deadline) {
        sleep(2);
        $ch = curl_init(BASE . '/scan/' . $scanId);
        curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
        $result = json_decode(curl_exec($ch), true);
        curl_close($ch);

        if ($result['status'] === 'completed') return $result;
        if ($result['status'] === 'failed')    throw new RuntimeException('Scan failed');
    }

    throw new RuntimeException('Scan timed out');
}

$result = scan_url('https://example.com');
echo $result['verdict'] . ' ' . $result['score'] . PHP_EOL;

Ruby

ruby
require 'net/http'
require 'json'
require 'uri'

API_KEY = 'usc_your_api_key_here'
BASE    = 'https://urlscanner.online/api'

def scan_url(url, timeout: 60)
  uri  = URI("#{BASE}/scan")
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true

  # 1. Submit
  req = Net::HTTP::Post.new(uri.path)
  req['X-API-Key']     = API_KEY
  req['Content-Type']  = 'application/json'
  req.body             = { url: url }.to_json

  data = JSON.parse(http.request(req).body)

  # Cached — free of charge
  return data['cachedResult'] if data['cached']

  scan_id  = data['scanId']
  deadline = Time.now + timeout

  # 2. Poll until completed
  while Time.now < deadline
    sleep 2
    res    = http.get("/api/scan/#{scan_id}")
    result = JSON.parse(res.body)

    return result if result['status'] == 'completed'
    raise 'Scan failed' if result['status'] == 'failed'
  end

  raise 'Scan timed out'
end

result = scan_url('https://example.com')
puts "#{result['verdict']} #{result['score']}"
# => clean 0