Documentation
Domain Intelligence
Concurrent WHOIS, DNS, SSL, HTTP header, screenshot, reputation, email security, subdomain, certificate transparency, BGP/ASN, technology fingerprinting, threat intelligence, and DNS security lookups for any domain — with an aggregate risk score in every response.
Authentication
All requests require an API key passed via the X-API-Key header:
X-API-Key: your-api-key-here
| Scenario | Response |
|---|---|
| Missing API key | 401 Unauthorized |
| Invalid API key | 401 Unauthorized |
| Feature not included in plan | 403 Forbidden |
Plans
Feature access and limits are determined by your API key's plan.
| Free | Pro | |
|---|---|---|
| Rate limit | 5 req/min | 120 req/min |
| Max domains per request | 1 | 20 |
| WHOIS, DNS, SSL, Headers | Included | Included |
| Email Security | Included | Included |
| Subdomains | Included | Included |
| CT Logs | Included | Included |
| BGP / ASN | Included | Included |
| Fingerprint | Included | Included |
| DNS Security (DNSSEC / CAA) | Included | Included |
| Aggregate Risk Score | Included | Included |
| Screenshot | -- | Pro |
| Reputation | -- | Pro |
| Threat Intelligence | -- | Pro |
Pricing
Get started with a free plan or upgrade for higher limits and premium features.
| Plan | Features | |
|---|---|---|
| Free | 5 req/min, 1 domain/request, all lookups except screenshot, reputation, and threat intel | Included with any API key |
| Pro | 120 req/min, 20 domains/request, all lookups including screenshot, reputation, and threat intel | Subscribe |
Already a subscriber? Manage your subscription
Rate Limiting
Requests are rate-limited per API key using a sliding window. Responses report your current window state:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per minute |
X-RateLimit-Remaining | Requests remaining in the current window |
Retry-After | Seconds until the window resets (only present on 429 responses) |
When the limit is exceeded, the API returns 429 Too Many Requests. Wait until the Retry-After period expires before retrying.
Domain Intelligence
Look up one or more domains concurrently. You choose exactly which lookups to run via the lookups array — there is no default set. All requested lookups run concurrently with a 30-second timeout.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
domains |
string[] |
Required | Domain names to look up. Max count depends on plan (1 / 5 / 20), counted on the entries you submit. Requesting subdomain enumeration lowers that cap — see Subdomain batch limits. |
lookups |
string[] |
Required | Which lookups to run. Must be non-empty — there is no default set and no "all" value. Names are trimmed, lowercased and deduplicated, so [" SSL ", "ssl"] is just ["ssl"]. |
Valid Lookups
| Name | Plan | Description |
|---|---|---|
whois | All | Registration, registrar, expiry, nameservers, contacts. |
dns | All | A, AAAA, MX, NS, TXT, and CNAME records. |
ssl | All | TLS certificate subject, issuer, validity window, SANs. |
headers | All | HTTP response headers and security header analysis. |
email_security | All | Check SPF, DMARC, and DKIM records. |
subdomains | All | Enumerate subdomains via Certificate Transparency logs. |
ct_logs | All | Query Certificate Transparency logs for issued certificates. |
bgp | All | Look up BGP/ASN information for the domain's IP addresses. |
fingerprint | All | Detect web technologies from HTTP response headers. Costs no extra network call — see Caching. |
dns_security | All | DNSSEC deployment (DS + DNSKEY + algorithms) and CAA records. |
risk | All | Weighted 0–100 aggregate risk score over the other lookups you requested. No network call, never cached — see Caching. |
screenshot | Pro | Capture a page screenshot. |
reputation | Pro | Check domain against threat blocklists. |
threat_intel | Pro | Query threat intelligence feeds (VirusTotal, URLhaus, AlienVault OTX). |
An unrecognized name returns 400 naming the offending entry. A lookup outside your plan returns 403.
Subdomain batch limits
Subdomain enumeration queries Certificate Transparency logs, which are slower and more rate-limited than the other sources, so a request that includes subdomains is held to a tighter domain cap than the same plan's usual one. The cap applies to the whole request: exceeding it returns 400 and nothing is looked up.
| Plan | Domains per request | With subdomains requested |
|---|---|---|
| Free | 1 | 1 |
| Pro | 20 | 5 |
To enumerate subdomains for a larger batch, split it into several requests, or run subdomains in its own request and the remaining lookups in another.
Domain Normalization
Domain names are normalized automatically. You can pass bare domains or full URLs with any scheme — the API extracts the hostname by stripping scheme prefixes, userinfo, ports, paths, query strings, and fragments, then trims whitespace and lowercases the result. Entries that normalize to the same domain are deduplicated into a single result. The domain field in the response holds the normalized form and inputs lists the original entries that mapped to it. Raw IP addresses are rejected — use the IP Intelligence endpoint instead.
| You send | We look up |
|---|---|
example.com | example.com |
https://example.com/path?q=1 | example.com |
ftp://example.com | example.com |
example.com:8080 | example.com |
user:pass@example.com | example.com |
Example.COM/ | example.com |
example.com | example.com |
Example
curl -X POST https://api.securityaccelerated.com/v1/domain/lookup \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your-api-key' \
-H 'X-Client-Request-ID: req-001' \
-d '{
"domains": ["example.com"],
"lookups": ["dns", "ssl", "email_security", "subdomains"]
}'
const res = await fetch("https://api.securityaccelerated.com/v1/domain/lookup", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": "your-api-key",
"X-Client-Request-ID": "req-001",
},
body: JSON.stringify({
domains: ["example.com"],
lookups: ["dns", "ssl", "email_security", "subdomains"],
}),
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(error);
}
// Results are wrapped in a "results" envelope. Match by `domain` rather than
// by position: duplicates collapse and ordering is not guaranteed.
const { results } = await res.json();
for (const r of results) {
// Every lookup carries its own cache_hit.
console.log(r.domain, r.dns?.a, r.dns?.cache_hit);
// Individual lookups can fail while the rest succeed.
for (const e of r.errors ?? []) {
console.warn(`${r.domain}: ${e.lookup} failed — ${e.message}`);
}
}
import requests
resp = requests.post(
"https://api.securityaccelerated.com/v1/domain/lookup",
headers={
"X-API-Key": "your-api-key",
"X-Client-Request-ID": "req-001",
},
json={
"domains": ["example.com"],
"lookups": ["dns", "ssl", "email_security", "subdomains"],
},
)
if resp.status_code != 200:
print("Error:", resp.json()["error"])
else:
# Results are wrapped in a "results" envelope.
for result in resp.json()["results"]:
print(result["domain"], result["dns"]["a"], result["dns"]["cache_hit"])
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
body, _ := json.Marshal(map[string]any{
"domains": []string{"example.com"},
"lookups": []string{"dns", "ssl", "email_security", "subdomains"},
})
req, _ := http.NewRequest("POST",
"https://api.securityaccelerated.com/v1/domain/lookup",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", "your-api-key")
req.Header.Set("X-Client-Request-ID", "req-001")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var apiErr map[string]string
json.NewDecoder(resp.Body).Decode(&apiErr)
fmt.Println("Error:", apiErr["error"])
return
}
// Results are wrapped in a "results" envelope.
var response struct {
Results []map[string]any `json:"results"`
}
json.NewDecoder(resp.Body).Decode(&response)
fmt.Println(response.Results[0]["domain"])
}
Response Structure
Results are wrapped in a results envelope, so every response — success or error — is a JSON object. The envelope leaves room for response-level fields to be added later without breaking clients.
{
"results": [
{
"domain": "example.com",
"inputs": ["HTTPS://Example.COM/", "example.com"],
"dns": { "cache_hit": true, "a": ["93.184.216.34"] },
"ssl": { "cache_hit": false, ... }
}
]
}
Identifying Results
results contains one element per unique normalized domain:
domain— the normalized form. Match results by this field.inputs— the original request entries, verbatim, that mapped to this domain. Use it to reconcile results against your request without reimplementing normalization.
Array position is not part of the API contract. Do not assume results[i] corresponds to domains[i] — duplicates collapse and ordering is not guaranteed.
Each result contains only the lookups you requested — never any you did not — plus domain and inputs, and errors when a lookup failed. A lookup that failed is omitted rather than returned empty, so you can get fewer fields than you asked for.
Per-Lookup cache_hit
Every lookup object carries its own cache_hit flag: true when that lookup was served from cache, false when it was fetched live. There is no top-level cache_hit — different fields in the same result routinely differ, because each lookup is cached separately with its own TTL. See Caching.
Invalid Domains
Invalid domains in a multi-domain request do not fail the entire request. They are included in the response with a validation error while valid domains return normally:
{
"results": [
{
"domain": "example.com",
"inputs": ["example.com"],
"whois": { "cache_hit": false, "domain_name": "example.com", ... },
"dns": { "cache_hit": true, ... }
},
{
"domain": "notadomain",
"inputs": ["notadomain"],
"errors": [{ "lookup": "validation", "message": "invalid domain name" }]
}
]
}
A request whose only domain is invalid returns 400.
Response Fields
whois -- Domain Registration
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
domain_name | string | The registered domain name |
registrar | string | Registrar (e.g., GoDaddy, Namecheap) |
created_date | string | Registration date -- new domains are higher risk |
updated_date | string | Last modification date |
expiry_date | string | Expiration date -- expiring domains are red flags |
status | string[] | EPP status codes (e.g., clientTransferProhibited) |
name_servers | string[] | Authoritative DNS servers |
registrant | object | Registrant contact (name, organization, country, email) |
admin | object | Administrative contact |
tech | object | Technical contact |
{
"domain_name": "example.com",
"registrar": "RESERVED-Internet Assigned Numbers Authority",
"created_date": "1995-08-14T04:00:00Z",
"updated_date": "2024-08-14T07:01:38Z",
"expiry_date": "2025-08-13T04:00:00Z",
"status": ["clientDeleteProhibited", "clientTransferProhibited"],
"name_servers": ["a.iana-servers.net", "b.iana-servers.net"],
"registrant": { "organization": "REDACTED FOR PRIVACY", "country": "US" }
}
dns -- DNS Records
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
a | string[] | IPv4 addresses |
aaaa | string[] | IPv6 addresses |
mx | object[] | Mail servers (host, priority) |
ns | string[] | Name servers |
txt | string[] | TXT records (SPF, verification tokens, etc.) |
cname | string | Canonical name alias |
{
"a": ["93.184.216.34"],
"aaaa": ["2606:2800:220:1:248:1893:25c8:1946"],
"mx": [{ "host": "mail.example.com", "priority": 10 }],
"ns": ["a.iana-servers.net", "b.iana-servers.net"],
"txt": ["v=spf1 -all"]
}
ssl -- TLS Certificate
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
subject | string | Entity the certificate was issued to |
issuer | string | Certificate authority |
not_before | string | Start of validity period |
not_after | string | End of validity period |
expired | bool | Whether the certificate has expired |
days_until_expiry | int | Days remaining until expiration |
sans | string[] | Subject Alternative Names -- all hostnames the cert covers |
serial_number | string | Certificate serial number |
signature_algorithm | string | Cryptographic algorithm (e.g., SHA-256) |
{
"subject": "www.example.org",
"issuer": "DigiCert Global G2 TLS RSA SHA256 2020 CA1",
"not_before": "2024-01-30T00:00:00Z",
"not_after": "2025-03-01T23:59:59Z",
"expired": false,
"days_until_expiry": 142,
"sans": ["www.example.org", "example.net", "example.org"],
"serial_number": "0F:BE:08:B0:85:4D:05:73:8A:B0...",
"signature_algorithm": "SHA256-RSA"
}
headers -- HTTP Response
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
status_code | int | HTTP status code (200, 301, 404, etc.) |
headers | object | Raw response headers as key-value pairs |
security_headers | object | Security header audit (see below) |
Security Headers Checked
| Field | HTTP Header |
|---|---|
strict_transport_security | Strict-Transport-Security (HSTS) |
content_security_policy | Content-Security-Policy (CSP) |
x_frame_options | X-Frame-Options |
x_content_type_options | X-Content-Type-Options |
x_xss_protection | X-XSS-Protection |
referrer_policy | Referrer-Policy |
permissions_policy | Permissions-Policy |
{
"status_code": 200,
"headers": {
"Server": "ECAcc (dcd/7D5A)",
"Content-Type": "text/html; charset=UTF-8",
"Cache-Control": "max-age=604800"
},
"security_headers": {
"strict_transport_security": "max-age=31536000",
"x_content_type_options": "nosniff",
"x_frame_options": "DENY"
}
}
screenshot -- Page Screenshot Pro
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
status_code | int | HTTP status code from probing the domain |
png | string | Base64-encoded PNG image (empty for non-200 responses) |
{
"status_code": 200,
"png": "iVBORw0KGgoAAAANSUhEUgAA..."
}
Screenshots are captured a few at a time
The other lookups are a network call at most — fingerprint and risk are pure computation over results you already asked for. A screenshot is a browser, and a browser wants roughly half a gigabyte of memory, so captures run under a small service-wide concurrency cap — typically one at a time — rather than in parallel. A request asking for screenshot waits its turn behind any capture already running — including captures from other customers.
If a capture has not started by the time the request deadline arrives, that lookup reports a queue timeout instead of the request piling up another browser:
{ "lookup": "screenshot", "message": "screenshot queue timed out, please retry" }
The rest of the response is unaffected — every other lookup you asked for still returns, and the status is still 200.
screenshot over a handful of domains rather than a full batch, and put it in its own request so a queue wait never delays the lookups that would have returned immediately.reputation -- Domain Reputation Pro
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
score | int | Risk score from 0 (clean) to 100 (maximum risk) |
risk | string | clean, low, medium, or high |
providers | array | Per-provider results |
Provider Result
| Field | Type | Description |
|---|---|---|
listed | bool | Whether the domain appears on this blocklist |
category | string | Classification if listed (e.g., phishing, malware) |
provider | string | Blocklist name |
return_code | string | Raw DNS return code (only when listed) |
{
"score": 35,
"risk": "medium",
"providers": [
{ "listed": true, "category": "phishing", "provider": "spamhaus_dbl", "return_code": "127.0.1.4" },
{ "listed": false, "provider": "surbl" },
{ "listed": false, "provider": "uribl" },
{ "listed": false, "provider": "spamhaus_zen" }
]
}
Providers
The following blocklists are checked: Spamhaus DBL, SURBL, URIBL, and Spamhaus ZEN.
Risk Levels
| Risk | Score Range |
|---|---|
| clean | 0 |
| low | 1 -- 29 |
| medium | 30 -- 59 |
| high | 60 -- 100 |
email_security -- Email Authentication
spf -- Sender Policy Framework
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
raw | string | Full SPF TXT record |
version | string | SPF version (typically spf1) |
mechanisms | string[] | Parsed mechanisms (include:, ip4:, etc.) |
all | string | Terminal qualifier: -all, ~all, ?all, or +all |
dmarc -- DMARC Policy
| Field | Type | Description |
|---|---|---|
raw | string | Full DMARC TXT record |
version | string | DMARC version (typically DMARC1) |
policy | string | none, quarantine, or reject |
pct | int | Percentage of messages the policy applies to |
rua | string | Aggregate report destination |
ruf | string | Forensic report destination |
sp | string | Subdomain policy |
dkim -- DKIM Records
| Field | Type | Description |
|---|---|---|
selector | string | Selector name probed |
found | bool | Whether a DKIM record exists for this selector |
raw | string | Full DKIM TXT record (only when found) |
mta_sts -- MTA Strict Transport Security
Only present when a record exists at _mta-sts.<domain>. When the TXT record is found, the policy file at https://mta-sts.<domain>/.well-known/mta-sts.txt is fetched for the mode and MX patterns.
| Field | Type | Description |
|---|---|---|
raw | string | Full MTA-STS TXT record |
id | string | Policy version identifier (id= tag) |
policy_fetched | bool | Whether the well-known policy file was retrieved |
mode | string | enforce, testing, or none (from the policy file) |
max_age | int | Policy cache lifetime in seconds |
mx_patterns | string[] | MX host patterns the policy allows |
tls_rpt -- SMTP TLS Reporting
Only present when a record exists at _smtp._tls.<domain>.
| Field | Type | Description |
|---|---|---|
raw | string | Full TLS-RPT TXT record |
rua | string | Report destination (e.g., mailto:tlsrpt@example.com) |
bimi -- Brand Indicators for Message Identification
Only present when a record exists at default._bimi.<domain>.
| Field | Type | Description |
|---|---|---|
raw | string | Full BIMI TXT record |
location | string | Brand logo SVG URL (l= tag) |
authority | string | Verified Mark Certificate URL (a= tag) |
{
"spf": {
"raw": "v=spf1 include:_spf.google.com -all",
"version": "spf1",
"mechanisms": ["include:_spf.google.com"],
"all": "-all"
},
"dmarc": {
"raw": "v=DMARC1; p=reject; rua=mailto:dmarc@example.com",
"version": "DMARC1",
"policy": "reject",
"rua": "mailto:dmarc@example.com"
},
"dkim": [
{ "selector": "google", "found": true, "raw": "v=DKIM1; k=rsa; p=MIGf..." },
{ "selector": "default", "found": false }
],
"mta_sts": {
"raw": "v=STSv1; id=20240101T000000",
"id": "20240101T000000",
"policy_fetched": true,
"mode": "enforce",
"max_age": 86400,
"mx_patterns": ["*.example.com"]
},
"tls_rpt": { "raw": "v=TLSRPTv1; rua=mailto:tlsrpt@example.com", "rua": "mailto:tlsrpt@example.com" },
"bimi": { "raw": "v=BIMI1; l=https://example.com/logo.svg", "location": "https://example.com/logo.svg" }
}
google, default, selector1, selector2, k1, s1, mail, dkim. The mta_sts, tls_rpt, and bimi objects are omitted when the corresponding DNS record does not exist.subdomains -- Subdomain Enumeration
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
count | int | Number of subdomains returned |
capped | bool | true if results were truncated at the cap (500) |
subdomains | array | Discovered subdomains |
Subdomain Entry
| Field | Type | Description |
|---|---|---|
domain | string | The subdomain |
source | string | Data source (crt.sh) |
{
"count": 3,
"capped": false,
"subdomains": [
{ "domain": "www.example.com", "source": "crt.sh" },
{ "domain": "mail.example.com", "source": "crt.sh" },
{ "domain": "api.example.com", "source": "crt.sh" }
]
}
ct_logs -- Certificate Transparency Logs
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
count | int | Number of certificate entries returned |
capped | bool | true if results were truncated at the cap (100) |
entries | array | Certificate log entries, sorted newest-first |
CT Log Entry
| Field | Type | Description |
|---|---|---|
logged_at | string | When the certificate was logged to CT |
not_before | string | Certificate validity start |
not_after | string | Certificate validity end |
issuer | string | Certificate Authority |
common_name | string | Certificate CN field |
name_value | string | All names covered by the certificate |
serial | string | Certificate serial number |
{
"count": 2,
"capped": false,
"entries": [
{
"logged_at": "2024-01-15T00:00:00",
"not_before": "2024-01-15T00:00:00",
"not_after": "2025-04-15T23:59:59",
"issuer": "Let's Encrypt",
"common_name": "example.com",
"serial": "abc123def456"
}
]
}
bgp -- BGP / ASN Enrichment
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
records | array | ASN records, deduplicated by ASN |
ASN Record
| Field | Type | Description |
|---|---|---|
ip | string | Resolved IP address |
asn | int | Autonomous System Number |
prefix | string | IP prefix (CIDR notation) |
country | string | Country code |
registry | string | Regional Internet Registry (arin, ripe, apnic, etc.) |
description | string | Organization name |
{
"records": [
{
"ip": "93.184.216.34",
"asn": 15133,
"prefix": "93.184.216.0/24",
"country": "US",
"registry": "arin",
"description": "EDGECAST, US"
}
]
}
fingerprint -- Technology Fingerprinting
Detects web servers, frameworks, CDNs, CMS platforms, and programming languages from HTTP response headers. No extra HTTP requests are made -- detection uses the headers already fetched, so requesting headers alongside it costs nothing extra. Approximately 90 technologies are covered.
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether the underlying headers data was served from cache. Fingerprint has no cache entry of its own -- see Caching |
count | int | Number of detected technologies |
technologies | array | Detected technologies |
Technology Entry
| Field | Type | Description |
|---|---|---|
name | string | Technology name (e.g., Nginx, PHP, Cloudflare) |
category | string | Category (see below) |
version | string | Version extracted from header value (empty if not detectable) |
confidence | string | Which signal matched: header or cookie |
{
"count": 2,
"technologies": [
{ "name": "Nginx", "category": "web-server", "version": "1.25.3", "confidence": "header" },
{ "name": "Cloudflare", "category": "cdn", "version": "", "confidence": "header" }
]
}
Categories
web-server, language, framework, cdn, cms, analytics, security, hosting, cache, javascript, other
threat_intel -- Threat Intelligence Pro
Queries external threat intelligence feeds concurrently and returns an aggregated risk assessment. Three providers are queried: VirusTotal (70+ antivirus engines), URLhaus (Abuse.ch malware URL database), and AlienVault OTX (community threat pulses).
| Field | Type | Description |
|---|---|---|
cache_hit | bool | Whether this lookup was served from cache (true) or fetched live (false) |
risk_level | string | Aggregate risk: none, low, medium, high, or critical |
score | int | Weighted aggregate score (0--100) |
categories | string[] | Merged threat categories across all providers |
providers | array | Per-provider results (see below) |
Provider Result
| Field | Type | Description |
|---|---|---|
provider | string | Provider name: virustotal, urlhaus, or alienvault_otx |
detected | bool | Whether the provider flagged this domain |
score | int | Provider-specific score (0--100). Omitted when not detected. |
categories | string[] | Threat categories from this provider. Omitted when not detected. |
details | string | Human-readable summary (e.g., "3/70 engines detected this domain"). Omitted when not detected. |
{
"risk_level": "medium",
"score": 45,
"categories": ["malware"],
"providers": [
{
"provider": "virustotal",
"detected": true,
"score": 3,
"categories": ["malware"],
"details": "3/70 engines detected this domain"
},
{
"provider": "urlhaus",
"detected": false
},
{
"provider": "alienvault_otx",
"detected": true,
"score": 20,
"categories": ["malware"],
"details": "5 threat pulses reference this domain"
}
]
}
Scoring
The aggregate score is a weighted average across providers: VirusTotal (50%), URLhaus (30%), AlienVault OTX (20%). Risk levels map from the aggregate score: 0 = none, 1--20 = low, 21--50 = medium, 51--80 = high, 81--100 = critical.
dns_security -- DNSSEC & CAA
Checks DNSSEC deployment (DS records at the parent zone, DNSKEY records at the zone, signing algorithms) and CAA records via raw DNS queries. Available to all plans. Missing DNSSEC means DNS responses can be spoofed; missing CAA means any certificate authority can issue certificates for the domain.
dnssec
| Field | Type | Description |
|---|---|---|
enabled | bool | true when both DS (parent) and DNSKEY (zone) records exist |
ds_records | int | Number of DS records at the parent zone |
dnskey_records | int | Number of DNSKEY records at the zone |
algorithms | string[] | Signing algorithms from DS records (e.g., ECDSAP256SHA256) |
caa
| Field | Type | Description |
|---|---|---|
present | bool | Whether any CAA records exist |
records | array | CAA records: flag (int), tag (issue, issuewild, or iodef), value |
Example
curl -X POST https://api.securityaccelerated.com/v1/domain/lookup \
-H 'Content-Type: application/json' \
-H 'X-API-Key: your-api-key' \
-d '{
"domains": ["example.com"],
"lookups": ["dns_security"]
}'
The dns_security object in the response:
{
"dnssec": {
"enabled": true,
"ds_records": 1,
"dnskey_records": 2,
"algorithms": ["ECDSAP256SHA256"]
},
"caa": {
"present": true,
"records": [
{ "flag": 0, "tag": "issue", "value": "letsencrypt.org" },
{ "flag": 0, "tag": "iodef", "value": "mailto:security@example.com" }
]
}
}
risk -- Aggregate Risk Score
A computed 0--100 weighted risk score. Only present when risk is included in lookups. It makes no extra network calls and is never cached — the score is derived from whichever other requested lookups landed on the response, renormalized over the weights of the components that have data. Its cache_hit is true only when every scoring input came from cache.
| Field | Type | Description |
|---|---|---|
score | int | Weighted aggregate risk score (0--100) |
risk_level | string | none, low, medium, high, or critical |
components | array | Per-component breakdown (see below) |
Risk Component
| Field | Type | Description |
|---|---|---|
component | string | Which signal contributed (see weights below) |
score | int | This component's 0--100 score |
weight | float | The component's weight in the aggregate |
detail | string | Human-readable explanation (e.g., certificate expires in 12 days) |
Component Weights
| Component | Weight | Signal |
|---|---|---|
threat_intel | 0.35 | Aggregate threat intelligence score (when requested) |
reputation | 0.25 | DNS blocklist reputation score (when requested) |
ssl | 0.10 | Certificate expired or expiring soon |
security_headers | 0.10 | Missing common security headers |
email_security | 0.10 | Missing or weak SPF / DMARC (when requested) |
domain_age | 0.10 | Recently registered domains score higher |
Components only contribute when their underlying lookup ran and returned data. A base lookup scores from SSL, security headers, and domain age alone; requesting threat_intel or reputation makes those signals dominate.
{
"score": 28,
"risk_level": "medium",
"components": [
{ "component": "ssl", "score": 40, "weight": 0.1, "detail": "certificate expires in 12 days" },
{ "component": "security_headers", "score": 20, "weight": 0.1, "detail": "missing: content-security-policy, referrer-policy" },
{ "component": "domain_age", "score": 25, "weight": 0.1, "detail": "domain registered 210 days ago" }
]
}
Risk Levels
| Risk Level | Score Range |
|---|---|
| none | 0 |
| low | 1 -- 20 |
| medium | 21 -- 50 |
| high | 51 -- 80 |
| critical | 81 -- 100 |
errors -- Partial Failures
Individual lookups can fail without blocking the rest of the response. Each error contains:
| Field | Type | Description |
|---|---|---|
lookup | string | Which lookup failed (e.g., whois, dns, ssl) |
message | string | Human-readable error description |
What the lookup name tells you
The name is either a plain lookup or <lookup>:<provider>, and the two mean different things:
| Shape | Example | What it means for the result |
|---|---|---|
| Plain name | whois |
The lookup failed outright. Its field is absent from the result |
| Qualified name | threat_intel:virustotal |
One provider inside a fan-out lookup failed while others answered. That lookup's field is present, scored over the providers that responded |
Only reputation and threat_intel fan out to providers, so only those produce qualified names. Treating every entry in errors as a missing field would throw away perfectly good partial results from those two.
A lookup that failed outright omits its field entirely
This is the case worth writing your client against. The domain is valid, the request succeeded, and one upstream did not answer — here a WHOIS server that timed out while DNS and SSL returned normally:
{
"results": [
{
"domain": "example.com",
"inputs": ["example.com"],
"dns": { "cache_hit": false, "a": ["93.184.216.34"], ... },
"ssl": { "cache_hit": true, "issuer": "DigiCert Inc", ... },
"errors": [
{ "lookup": "whois", "message": "i/o timeout" }
]
}
]
}
There is no whois key at all — not null, not an empty object. Requesting n lookups does not guarantee n fields in the response, so code that reaches straight for result.whois.registrar works in testing and throws the first time an upstream has a bad minute. Contrast a reputation:spamhaus_dbl entry, where reputation is still there with the other providers' verdicts.
errors and retrying that domain is usually better than treating an absent field as an absent answer — the two look identical in the response, and only errors tells them apart.Caching
Results are cached per lookup, per domain — not per response. Each lookup object in the response carries its own cache_hit flag, so a single result routinely mixes cached and freshly fetched fields.
Because each lookup is cached independently, the combination you request has no effect on reuse: a dns-only request warms the same entry a later dns + ssl request reads from.
TTLs
Every lookup has its own TTL, reflecting how fast that data actually changes. Expiry is absolute — an expired entry is gone and the lookup is refetched. There is no stale serving.
| Lookup | TTL |
|---|---|
whois, ct_logs, subdomains, bgp | 24 hours |
ssl, email_security, dns_security | 4 hours |
screenshot | 60 minutes |
dns, headers, reputation, threat_intel | 30 minutes |
fingerprint and risk are never cached separately
Both are pure computation over data already fetched for other lookups, so they cost no network call and get no cache entry of their own.
risk scores whichever of threat_intel, reputation, ssl, headers, email_security and whois you also requested. Its cache_hit is true only when every scoring input came from cache.
Technology fingerprinting is computation over the headers payload, so caching headers already caches it. That means:
- Requesting
headersandfingerprinttogether fetches headers exactly once — no extra network call. - Requesting
fingerprintalone fetches and caches headers internally, but theheadersobject is not returned. fingerprint.cache_hitreflects where its headers input came from.- Fingerprint rule updates take effect immediately — there is no stale fingerprint TTL to wait out.
Cache failures never fail a request
Caching is fail-open. If the cache is unreachable, every lookup is treated as a miss and fetched live. If a cached entry is unreadable, it is treated as a miss and overwritten by the fresh result. If a cache write fails, the lookup still succeeds and is returned normally. Cache problems never appear in the errors array — that is reserved for actual lookup failures.
Error Handling
| Status | Meaning |
|---|---|
400 Bad Request | Invalid request body, missing domains field, or invalid domain name |
400 Bad Request | lookups missing or empty after normalization — lookups is required, e.g. ["dns", "ssl"] |
400 Bad Request | Unrecognized lookup name — the message names the offending entry |
400 Bad Request | More domains than your plan allows |
400 Bad Request | More domains than the tighter cap that applies when subdomains is requested — see Subdomain batch limits |
401 Unauthorized | Missing or invalid API key |
403 Forbidden | Your plan does not include this endpoint, or a requested lookup — e.g. screenshot requires a pro plan |
413 Content Too Large | Request body exceeds the 1 MB limit |
429 Too Many Requests | Rate limit exceeded -- check the Retry-After header |
500 Internal Server Error | Server error -- contact support@securityaccelerated.com |
Error Response Format
All error responses use this format:
{
"error": "description of the problem"
}
200 OK with the successful lookups populated and failures listed in the errors array.Request Correlation
Every response carries an X-Request-ID the API generates for you. You can additionally send your own X-Client-Request-ID to tie a request to an identifier your systems already use — it is echoed back and recorded in server-side logs.
| Header | Direction | Description |
|---|---|---|
X-Client-Request-ID | Request | Your correlation ID. Up to 64 characters, limited to letters, digits, hyphens, underscores, dots, and colons — enough for UUIDs and common trace IDs. |
X-Client-Request-ID | Response | Echoed back byte for byte when the value you sent was accepted. |
X-Request-ID | Response | A UUID the API generates for every request, whether or not you sent a correlation ID. Quote this when contacting support. |
X-Client-Request-ID on the response as a signal that the value did not pass validation. X-Request-ID is always present — including on error responses, where correlation matters most — so it works as a fallback.Full Response Example
Response Headers
Returned alongside the body on a successful lookup:
Content-Type: application/json; charset=utf-8
X-Request-ID: 9f8b2c14-6d3a-4e57-b8f1-2a7c05e91d34
X-Client-Request-ID: req-001
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-Request-ID is generated by the API for every request — quote it when contacting support. X-Client-Request-ID appears only when you sent a valid one; see Request Correlation. The X-RateLimit-* pair reflects your plan's window, and a 429 adds Retry-After — see Rate Limiting.
Response Body
A complete response for "lookups": ["whois", "dns", "ssl", "headers", "screenshot", "reputation", "email_security", "subdomains", "ct_logs", "bgp", "fingerprint", "threat_intel"]:
{
"results": [
{
"domain": "example.com",
"inputs": ["example.com"],
"whois": {
"cache_hit": false,
"domain_name": "example.com",
"registrar": "RESERVED-Internet Assigned Numbers Authority",
"created_date": "1995-08-14T04:00:00Z",
"expiry_date": "2025-08-13T04:00:00Z",
"name_servers": ["a.iana-servers.net", "b.iana-servers.net"]
},
"dns": {
"cache_hit": true,
"a": ["93.184.216.34"],
"aaaa": ["2606:2800:220:1:248:1893:25c8:1946"],
"ns": ["a.iana-servers.net", "b.iana-servers.net"],
"txt": ["v=spf1 -all"]
},
"ssl": {
"cache_hit": false,
"subject": "www.example.org",
"issuer": "DigiCert Global G2 TLS RSA SHA256 2020 CA1",
"not_after": "2025-03-01T23:59:59Z",
"expired": false,
"days_until_expiry": 142,
"sans": ["www.example.org", "example.net", "example.org"]
},
"headers": {
"cache_hit": false,
"status_code": 200,
"headers": { "Server": "ECAcc (dcd/7D5A)" },
"security_headers": {
"strict_transport_security": "max-age=31536000",
"x_content_type_options": "nosniff"
}
},
"screenshot": {
"cache_hit": false,
"status_code": 200,
"png": "iVBORw0KGgoAAAANSUhEUgAA..."
},
"reputation": {
"cache_hit": false,
"score": 0,
"risk": "clean",
"providers": [
{ "listed": false, "provider": "spamhaus_dbl" },
{ "listed": false, "provider": "surbl" },
{ "listed": false, "provider": "uribl" },
{ "listed": false, "provider": "spamhaus_zen" }
]
},
"email_security": {
"cache_hit": true,
"spf": { "raw": "v=spf1 -all", "version": "spf1", "all": "-all" },
"dmarc": { "raw": "v=DMARC1; p=reject", "version": "DMARC1", "policy": "reject" },
"dkim": [{ "selector": "default", "found": false }]
},
"subdomains": {
"cache_hit": false,
"count": 2,
"capped": false,
"subdomains": [
{ "domain": "www.example.com", "source": "crt.sh" },
{ "domain": "mail.example.com", "source": "crt.sh" }
]
},
"ct_logs": {
"cache_hit": false,
"count": 1,
"capped": false,
"entries": [
{ "logged_at": "2024-01-15T00:00:00", "issuer": "Let's Encrypt", "common_name": "example.com", "serial": "abc123" }
]
},
"bgp": {
"cache_hit": false,
"records": [
{ "ip": "93.184.216.34", "asn": 15133, "prefix": "93.184.216.0/24", "country": "US", "description": "EDGECAST, US" }
]
},
"fingerprint": {
"cache_hit": false,
"count": 1,
"technologies": [
{ "name": "ECAcc", "category": "cdn", "version": "", "confidence": "header" }
]
},
"threat_intel": {
"cache_hit": false,
"risk_level": "none",
"score": 0,
"providers": [
{ "provider": "virustotal", "detected": false },
{ "provider": "urlhaus", "detected": false },
{ "provider": "alienvault_otx", "detected": false }
]
}
}
]
}
Note the per-lookup cache_hit flags: dns and email_security came from cache here while the rest were fetched live. fingerprint reports the cache state of the headers data it was computed from.