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.
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.
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).
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
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/scan | Required | Submit URL — async, returns a scanId to poll |
| GET | /api/scan/{scanId} | None | Poll for completed async scan result |
| POST | /api/scan/sync | Required | Synchronous — 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
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | URL, domain, or IP address to scan |
| rescan | boolean | No | Force a fresh scan, bypassing the 7-day cache |
Response — fresh scan
{
"scanId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"url": "https://example.com",
"normalizedUrl": "https://example.com/",
"hostname": "example.com",
"remaining": 499,
"plan": "team"
}Response — cached result
{
"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
| Field | Type | Description |
|---|---|---|
| status | string | "running" | "completed" | "failed" |
| score | number | Threat score 0–100 (higher = more dangerous) |
| verdict | string | "clean" | "low" | "medium" | "high" | "critical" |
| dns | object | null | DNS records: A, AAAA, MX, TXT, NS, SOA, CAA |
| ssl | object | null | TLS certificate chain, protocol version, cipher |
| http | object | null | HTTP response headers and security header analysis |
| whois | object | null | Domain registration, registrar, age, expiry |
| threats | object | null | URLhaus, Spamhaus, SURBL blocklist results |
| screenshots | array | null | Screenshot URLs at desktop / mobile / tablet viewports |
| aiAnalysis | object | null | AI-generated risk summary, confidence, key findings |
| scanDurationMs | number | null | Total 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.
| Module | Included | Timeout |
|---|---|---|
| DNS | ✓ | 8s |
| HTTP Headers | ✓ | 10s |
| WHOIS | ✓ | 8s |
| SSL | ✓ | 8s |
| Threat checks | ✓ | 12s |
| 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.
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
{
"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:
| Method | POST |
| URL | https://urlscanner.online/api/scan/sync |
| Header | X-API-Key: usc_your_key |
| Content-Type | application/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.
| Plan | API scans / day | Price |
|---|---|---|
| Free | 10 / day (sync only) | €0 |
| Solo | 100 / day (sync only) | €29 / mo |
| Team | 500 / day | €99 / mo |
| Business | 2,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.
| Status | Error | Meaning |
|---|---|---|
| 400 | Invalid URL / blocked | URL is malformed, private, or on an SSRF blocklist |
| 401 | Invalid API key | X-API-Key header is missing or the key is revoked |
| 403 | Plan required | Your current plan does not include API access |
| 429 | Daily limit reached | Quota exhausted — check resetAt to know when it refills |
| 500 | Internal server error | Unexpected server-side failure |
Example error body
{
"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
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
doneJavaScript / 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" 0Python
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
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
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
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