Security Accelerated

Documentation

IP Intelligence

Look up any public IP address for geographic location, ASN, reverse DNS, abuse contacts, and threat intelligence.

https://api.securityaccelerated.com v0.74.1
support@securityaccelerated.com

Authentication

All requests require an API key passed via the X-API-Key header:

X-API-Key: your-api-key-here

Rate Limiting

Requests are rate-limited per API key using a sliding window. Responses report your current window state:

HeaderDescription
X-RateLimit-LimitMaximum requests per minute for your plan
X-RateLimit-RemainingRequests remaining in the current window
Retry-AfterSeconds to wait before retrying (only on 429)
PlanRate LimitMax IPs / Request
Free5 req/min1
Pro120 req/min20

IP Intelligence

POST /v1/ip/lookup

Submit one or more IP addresses and choose exactly which lookups to run via the lookups array — there is no default set. All requested lookups run concurrently.

Request Body

FieldTypePlanDescription
ipsstring[]AllArray of IPv4 or IPv6 addresses (required). Canonicalized before use, so 2001:DB8::1 and 2001:db8:0:0:0:0:0:1 are one address, one result, one cache key. Entries that canonicalize to the same address are deduplicated; plan limits count the entries you submitted.
lookupsstring[]AllWhich lookups to run (required, non-empty). Names are trimmed, lowercased and deduplicated.

Valid Lookups

NamePlanDescription
geoAllCountry, city, region, coordinates, timezone (MaxMind GeoLite2).
asnAllASN number, organization, network (MaxMind GeoLite2 ASN).
reverse_dnsAllPTR record hostnames.
abuse_contactAllNetwork owner, country, abuse email (via RDAP).
threat_intelProVirusTotal, URLhaus, and AlienVault OTX with a composite risk score.

The IP and domain catalogs are separate. threat_intel appears in both; every other name belongs to one endpoint only, so asking for whois on an IP returns 400.

Input Validation

IP addresses are validated and canonicalized using Go's netip.ParseAddr. The following are rejected with a 400 response:

Example

curl -X POST https://api.securityaccelerated.com/v1/ip/lookup \
  -H 'Content-Type: application/json' \
  -H 'X-API-Key: your-api-key' \
  -H 'X-Client-Request-ID: req-001' \
  -d '{
    "ips": ["8.8.8.8", "1.1.1.1"],
    "lookups": ["geo", "asn", "reverse_dns"]
  }'
const res = await fetch("https://api.securityaccelerated.com/v1/ip/lookup", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "your-api-key",
    "X-Client-Request-ID": "req-001",
  },
  body: JSON.stringify({
    ips: ["8.8.8.8", "1.1.1.1"],
    lookups: ["geo", "asn", "reverse_dns"],
  }),
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(error);
}

// Results are wrapped in a "results" envelope. Match by `ip` rather than by
// position: addresses that canonicalize to the same value collapse into one.
const { results } = await res.json();

for (const r of results) {
  console.log(r.ip, r.geo?.country, r.asn?.organization);

  // Individual lookups can fail while the rest succeed.
  for (const e of r.errors ?? []) {
    console.warn(`${r.ip}: ${e.lookup} failed — ${e.message}`);
  }
}
import requests

resp = requests.post(
    "https://api.securityaccelerated.com/v1/ip/lookup",
    headers={
        "X-API-Key": "your-api-key",
        "X-Client-Request-ID": "req-001",
    },
    json={
        "ips": ["8.8.8.8", "1.1.1.1"],
        "lookups": ["geo", "asn", "reverse_dns"],
    },
)

if resp.status_code != 200:
    print("Error:", resp.json()["error"])
else:
    # Results are wrapped in a "results" envelope.
    for r in resp.json()["results"]:
        print(r["ip"], r["geo"]["country"], r["asn"]["organization"])
package main

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

func main() {
    body, _ := json.Marshal(map[string]any{
        "ips":     []string{"8.8.8.8", "1.1.1.1"},
        "lookups": []string{"geo", "asn", "reverse_dns"},
    })

    req, _ := http.NewRequest("POST",
        "https://api.securityaccelerated.com/v1/ip/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]["ip"], response.Results[0]["geo"])
}

Response Fields

Results are wrapped in a results envelope. Match elements by ip rather than by position — addresses that canonicalize to the same value collapse into one result.

geo -- Geographic Location

Location data from MaxMind GeoLite2. Available on all plans.

FieldDescription
continent / continent_codeContinent name and two-letter code
country / country_codeCountry name and ISO 3166-1 alpha-2 code
region / region_codeFirst-level subdivision (state/province)
cityCity name
postal_codePostal/ZIP code
latitude / longitudeApproximate coordinates
timezoneIANA timezone (e.g. America/New_York)
accuracy_radiusAccuracy radius in kilometers

asn -- Autonomous System

Network ownership data from MaxMind GeoLite2 ASN. Available on all plans.

FieldDescription
asnAutonomous System Number
organizationOrganization that owns the ASN (e.g. "Google LLC")
networkCIDR block for the network

reverse_dns -- PTR Records

Reverse DNS lookup via PTR records. Available on all plans.

FieldDescription
hostnamesArray of hostnames from PTR records (e.g. ["dns.google"])

abuse_contact -- Network Abuse Contact

Abuse contact information from RDAP (Registration Data Access Protocol). Available on all plans.

FieldDescription
networkIP range (e.g. "8.8.8.0 - 8.8.8.255")
organizationNetwork owner organization
countryCountry of the network registration
emailAbuse contact email address

threat_intel -- Threat Intelligence Pro

Aggregated threat intelligence from multiple providers. Requires a Pro plan.

FieldDescription
risk_levelAggregated risk: none, low, medium, high, critical
scoreNumeric score (0-100)
categoriesDetected categories (e.g. "malicious", "suspicious")
providersPer-provider results with individual scores and details

Providers: VirusTotal (50% weight), URLhaus (30%), AlienVault OTX (20%).

errors -- Partial Failures

If individual lookups fail, they appear here. The rest of the response is still populated.

"errors": [
  { "lookup": "reverse_dns", "message": "ptr lookup: no such host" }
]

A lookup that failed outright omits its field entirely

The IP is valid, the request succeeded, and one lookup did not answer. The field it would have filled is absent — not null, not an empty object:

{
  "results": [
    {
      "ip": "8.8.8.8",
      "inputs": ["8.8.8.8"],
      "geo": { "cache_hit": false, "country": "US", ... },
      "asn": { "cache_hit": false, "asn": 15169, ... },
      "errors": [
        { "lookup": "abuse_contact", "message": "rdap lookup: i/o timeout" }
      ]
    }
  ]
}

Requesting n lookups does not guarantee n fields, so code that reaches straight for result.abuse_contact.email works in testing and throws the first time an upstream has a bad minute. Failures are never cached — the next request retries rather than remembering the failure.

One exception, and it is the useful kind: threat_intel fans out to several providers and reports a failing one as threat_intel:virustotal — a name qualified with a colon. That entry means one provider was unavailable, not that the lookup failed: threat_intel is still present, scored over the providers that did answer. A plain threat_intel means the whole lookup failed and the field is gone.

Error Handling

StatusMeaning
400 Bad RequestInvalid request body, missing ips field, or non-routable IP address
400 Bad Requestlookups missing or empty after normalization — lookups is required, e.g. ["geo", "asn"]
400 Bad RequestUnrecognized lookup name (including domain-only names such as whois) — the message names the offending entry
401 UnauthorizedMissing or invalid API key
403 ForbiddenA requested lookup is outside your plan — e.g. threat_intel requires a pro plan
413 Content Too LargeRequest body exceeds the 1 MB limit
429 Too Many RequestsRate limit exceeded -- check the Retry-After header
500 Internal Server ErrorServer error -- contact support@securityaccelerated.com

All error responses use this format:

{
  "error": "description of the problem"
}
Partial lookup failures are different from HTTP errors. When individual lookups fail (e.g., reverse DNS timeout), the request still returns 200 OK with successful lookups populated and failures listed in the errors array.

Caching

Results are cached per lookup, per IP under ip:<ip>:<lookup> — a namespace separate from domain lookups, so threat_intel on the two endpoints never collides. Each lookup object carries its own cache_hit, so one result can mix cached and freshly fetched fields.

LookupTTL
abuse_contact24 hours
reverse_dns6 hours
threat_intel30 minutes
geo, asnnever cached

geo and asn are never cached

Both read a local MaxMind memory-mapped database with no network call, so a Redis round trip would cost more than simply doing the lookup again. They are recomputed on every request and their cache_hit is always false — which is accurate, since the data really was produced for that request.

Cache failures never fail a request

Caching is fail-open: an unreachable cache means every lookup is fetched live, an unreadable entry is treated as a miss and overwritten, and a failed write still returns the fetched result. Failed lookups are never cached — a failing lookup is retried on the next request rather than remembered.

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.

HeaderDirectionDescription
X-Client-Request-IDRequestYour 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-IDResponseEchoed back byte for byte when the value you sent was accepted.
X-Request-IDResponseA UUID the API generates for every request, whether or not you sent a correlation ID. Quote this when contacting support.
A correlation ID that is too long or contains other characters is ignored rather than rejected — the request succeeds normally, but nothing is echoed back. If you are relying on the header for tracing, treat a missing 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

Results are wrapped in a results envelope. Each element carries the canonicalized ip and the verbatim inputs that mapped to it — match results by ip, not by array position. Every lookup object carries its own cache_hit; note that geo and asn are always false because they are never cached.

{
  "results": [
  {
    "ip": "8.8.8.8",
    "inputs": ["8.8.8.8"],
    "geo": {
      "cache_hit": false,
      "continent": "North America",
      "continent_code": "NA",
      "country": "United States",
      "country_code": "US",
      "region": "California",
      "region_code": "CA",
      "city": "Mountain View",
      "latitude": 37.386,
      "longitude": -122.0838,
      "timezone": "America/Los_Angeles",
      "accuracy_radius": 1000
    },
    "asn": {
      "cache_hit": false,
      "asn": 15169,
      "organization": "GOOGLE"
    },
    "reverse_dns": {
      "cache_hit": true,
      "hostnames": ["dns.google"]
    },
    "abuse_contact": {
      "cache_hit": true,
      "network": "8.8.8.0 - 8.8.8.255",
      "organization": "Google LLC",
      "country": "US",
      "email": "network-abuse@google.com"
    }
  }
  ]
}