Skip to content

API Reference

This page is the technical reference for the IP Intelligence API: base URL, authentication, request and response schemas, every stable error code, and pagination and rate-limit mechanics.

It assumes you have already read Using the API and have a key. The query syntax — supported search fields, combination rules, and what each one searches — is defined once, in Supported searches; this page does not repeat it, only how to send and read it over HTTP.

Base URL

https://api.ip.alphastrike.io

Every route below is under /v1. There is currently one version; a route or field is not removed or repurposed within it.

Authentication

Send your key as a bearer credential on every request:

Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL

A request with no Authorization header, a malformed one, or a key that is unknown or has been revoked all answer the same 401 unauthorized — the response does not distinguish which, so it cannot be used to test whether a particular key string is valid. See Creating a key and Allowance, revocation, and security for how keys are created, revoked, and should be handled.

Request conventions

  • Send Content-Type: application/json with every request body. GET routes take no body.
  • A body must be valid JSON, at most 512 KB, and contain only the fields documented for that request. An unrecognized field is rejected rather than silently ignored — build your client to send only the fields shown below, so a future optional response field does not require a client change.
  • Every response — success or error — carries a request_id string in the body and the same value in the X-Request-ID response header. Include it when reporting a problem.
  • Timestamps are RFC 3339, always UTC (a trailing Z, never a UTC offset).

Rate limits and the daily allowance

Only POST /v1/search draws on the daily allowance described in How much you can search. GET /v1/allowance, GET /v1/search/capabilities, POST /v1/search/count, POST /v1/search/facets, POST /v1/search/exposure, and POST /v1/hosts/detail never consume it, regardless of outcome — call them as often as you need.

Within POST /v1/search, a request is charged only when it answers with 200 — including a successful response with zero hosts, because that is an answer too. Every other outcome — a validation error, an auth or plan problem, a rate or capacity limit, a timeout, or an expired cursor — costs nothing and can be retried immediately without spending your allowance.

Read the allowance object on every 200 response instead of polling GET /v1/allowance between searches; it already reflects the search you just ran. Poll GET /v1/allowance only when you have not searched recently and want to check before you do — asking never spends one.

A separate, independent limit caps how many searches your account can have running at the same time; a request beyond it answers 429 user_concurrency_reached and, like every non-200 outcome, is free to retry once an earlier request finishes.

Pagination

POST /v1/search returns next_cursor when more results exist: an opaque sc1_ handle, 36 characters in total. Send it back as cursor in the next request with the same query and limit to get the following page. It is:

  • bound to that exact query and limit — changing either while paging answers 422 validation_failed;
  • one-time — it cannot be replayed once used;
  • short-lived — it expires after a bounded window.

A missing, expired, replayed, or otherwise invalid cursor answers 410 cursor_gone. Start over from an unset cursor; the fresh first page is a normal, chargeable search, but the expired attempt itself was free. A page holds at most 100 hosts, ordered ascending by IP address, so paging through the same query in the same order always produces the same sequence of hosts.

POST /v1/hosts/detail paginates a single host's own services with its own, separate continuation string (up to 512 characters, no fixed format), scoped to that one IP and returned only from that endpoint. It is unrelated to the sc1_ search cursor and the two are not interchangeable.

Endpoints

GET /v1/search/capabilities

The current supported-protocol registry — the same list Add filter loads. Call it rather than hardcoding protocol names, and use catalog_version to detect when the list has changed instead of refetching on a timer; the response is not intended for an HTTP cache (Cache-Control: no-store).

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     https://api.ip.alphastrike.io/v1/search/capabilities
Field Type Notes
request_id string
query_registry_version string Currently the fixed value service-v5.
catalog_version string Opaque; changes when the supported set changes.
protocols[] array Exactly 87 entries today, sorted lexicographically.
protocols[].value string Canonical lowercase token, for example https.
protocols[].ports[] integer[] Sorted, unique ports this token's catalog covers.
protocols[].ipv4 / .ipv6 boolean Whether the token applies to that address family.
protocols[].evidence_kind string summary, http, or ssh — which typed evidence shape Selected host shows for it.
{
  "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a",
  "query_registry_version": "service-v5",
  "catalog_version": "2026-08-01-v7",
  "protocols": [
    { "value": "http", "ports": [80, 8000, 8080], "ipv4": true, "ipv6": true, "evidence_kind": "http" },
    { "value": "https", "ports": [443, 8443], "ipv4": true, "ipv6": true, "evidence_kind": "http" },
    { "value": "ssh", "ports": [22], "ipv4": true, "ipv6": true, "evidence_kind": "ssh" }
  ]
}

(showing 3 of 87 — the response is not truncated; fetch it to see the current full list.)

GET /v1/allowance

Your remaining daily searches, without spending one. Covered fully in Allowance, revocation, and security:

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     https://api.ip.alphastrike.io/v1/allowance
{ "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a", "allowance": { "limit": 20, "used": 6, "remaining": 14, "resets_at": "2026-08-12T00:00:00Z" } }

If a response has no allowance field at all — here or on POST /v1/search — no daily limit applies to that account. That is not the same as none remaining.

POST /v1/search

The matching hosts for a query, exactly like the search box. limit defaults to 25 and accepts up to 100. See Supported searches for the query grammar.

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     -H "Content-Type: application/json" \
     -d '{"query":"organization:\"Alpha Strike\" protocol:https","limit":2}' \
     https://api.ip.alphastrike.io/v1/search
Field Type Notes
query string 1–512 characters.
limit integer 1–100, default 25.
cursor string From a previous response's next_cursor; see Pagination.

The response is one entry per unique IP. services.items is the bounded five-row preview described in Matching services; a service carries match_reasons only when your query named a service, port, product, certificate, CPE, CVE, CVSS, or exploit clause. organization_matches, domain_matches, and asn_matches appear only for that respective search type.

{
  "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a",
  "results": [
    {
      "ip": "203.0.113.10",
      "location": { "country_code": "DE", "country_name": "Germany", "city": "Frankfurt am Main", "latitude": 50.1109, "longitude": 8.6821, "accuracy_radius_km": 50 },
      "network": { "asn": 64496, "organization": "Alpha Strike Labs GmbH" },
      "whois": { "organization": "Alpha Strike Labs GmbH", "netname": "ALPHASTRIKE-NET", "cidr": "203.0.113.0/24", "registry": "ripe", "country_code": "DE" },
      "reverse_dns": "host-10.example.com",
      "domains": { "items": [
        { "name": "host-10.example.com", "source": "reverse_dns" },
        { "name": "www.example.com", "source": "certificate_subject" }
      ], "has_more": false },
      "services": {
        "items": [
          {
            "port": 443,
            "protocol": "https",
            "observed_at": "2026-08-10T03:14:00Z",
            "product": "F5 NGINX",
            "version": "1.26.1",
            "certificate_names": ["example.com"],
            "vulnerabilities": { "cve_count": 0, "has_exploits": false },
            "evidence": [
              { "kind": "http", "status_code": 200, "title": "Example", "server": "nginx",
                "tls": { "version": "TLSv1.3", "cipher_suite": "TLS_AES_128_GCM_SHA256", "issuer_common_name": "Example Fixture CA", "valid_from": "2026-06-01T00:00:00Z", "valid_until": "2026-08-30T00:00:00Z", "fingerprint_sha256": "b1946ac92492d2347c6235b4d2611184b1946ac92492d2347c6235b4d2611184", "browser_trusted": true } }
            ],
            "match_reasons": ["protocol:https"]
          }
        ],
        "has_more": false
      },
      "organization_matches": [
        { "source": "as_organization", "value": "Alpha Strike Labs GmbH" }
      ]
    }
  ],
  "next_cursor": "sc1_ab12CD34ef56GH78ij90KL12mn34OP56",
  "allowance": { "limit": 20, "used": 7, "remaining": 13, "resets_at": "2026-08-12T00:00:00Z" }
}

A domain query adds domain_resolution at the top level instead of organization_matches, and each result carries domain_matches instead:

{
  "domain_resolution": { "status": "resolved", "address_count": 2 },
  "results": [
    { "ip": "203.0.113.10", "services": { "items": [], "has_more": false },
      "domains": { "items": [], "has_more": false },
      "domain_matches": [ { "source": "dns_resolution", "value": "example.com" } ] }
  ]
}

POST /v1/search/count

An approximate unique-host estimate for an ASN or a filter query, independent of any loaded page — what the result count above a filter search shows. Not accepted for exact-IP, CIDR, or domain queries.

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     -H "Content-Type: application/json" \
     -d '{"query":"asn:64496"}' \
     https://api.ip.alphastrike.io/v1/search/count
{ "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a", "unique_hosts": { "value": 128, "relation": "approximate" } }

relation is exact only where the underlying count genuinely is one; treat approximate as an estimate, not an off-by-a-little count.

POST /v1/search/facets

The Suggested Filters behind a service, organization, or country search — six fixed groups, described in Suggested Filters. Returns facets_not_supported for exact-IP, CIDR, domain, or ASN queries, matching the guide.

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     -H "Content-Type: application/json" \
     -d '{"query":"protocol:https"}' \
     https://api.ip.alphastrike.io/v1/search/facets
{
  "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a",
  "partial": true,
  "can_add_filters": true,
  "groups": [
    { "field": "port", "status": "available", "items": [ { "value": "443", "clause": "port:443", "selected": false, "unique_hosts": { "value": 842, "relation": "approximate" } } ] },
    { "field": "protocol", "status": "available", "items": [ { "value": "https", "clause": "protocol:https", "selected": true, "unique_hosts": { "value": 1240, "relation": "approximate" } } ] },
    { "field": "product", "status": "available", "items": [ { "value": "F5 NGINX", "clause": "product:\"F5 NGINX\"", "selected": false, "unique_hosts": { "value": 210, "relation": "approximate" } } ] },
    { "field": "country", "status": "available", "items": [ { "value": "DE", "clause": "country:DE", "selected": false, "unique_hosts": { "value": 96, "relation": "approximate" } } ] },
    { "field": "cvss", "status": "unavailable", "items": [], "error_code": "facet_unavailable" },
    { "field": "exploit", "status": "timed_out", "items": [], "error_code": "facet_timeout" }
  ]
}

Each of the 6 groups is independent: partial: true here means the cvss and exploit groups did not come back, not that the whole response failed. Use the available groups and, if you want the rest, retry the request — a retry costs nothing, since this route is never metered. can_add_filters turns false once three filter clauses are already active, matching the limit in Supported searches.

POST /v1/search/exposure

The bounded geographic aggregation behind the Globe view, described in Visualize geographic exposure. Not available for domain queries yet.

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     -H "Content-Type: application/json" \
     -d '{"query":"organization:\"Alpha Strike\"","resolution":"regional"}' \
     https://api.ip.alphastrike.io/v1/search/exposure
Field Type Notes
query string Any supported non-domain search.
resolution string global (default), regional, or local — coarsest to finest.
{
  "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a",
  "unique_hosts": { "value": 842, "relation": "approximate" },
  "service_count": 1310,
  "resolution": "regional",
  "geotile_precision": 6,
  "locations_relation": "complete",
  "locations": [
    { "key": "6/33/22", "latitude": 50.11, "longitude": 8.68, "unique_hosts": 41, "service_count": 63, "country_code": "DE", "country_count": 1 },
    { "key": "6/34/22", "latitude": 48.85, "longitude": 2.35, "unique_hosts": 12, "service_count": 18 }
  ]
}

key is an opaque bucket address (an Elasticsearch geotile coordinate), not a place name — label a cluster from country_code, never from key or from the coordinates. The second bucket above omits country_code and country_count on purpose: that is the documented case where no host in the bucket carries a GeoIP country, and a client must fall back to the coordinates rather than inventing a country. locations_relation: "bounded" means the 500-bucket response limit was reached; unique_hosts and service_count still describe the complete matching result set either way.

POST /v1/hosts/detail

The complete current service inventory for one exact IP — the same data behind Host investigation and Selected host. Never metered, so open it as often as you like.

curl -s -H "Authorization: Bearer ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL" \
     -H "Content-Type: application/json" \
     -d '{"ip":"203.0.113.10","limit":2}' \
     https://api.ip.alphastrike.io/v1/hosts/detail
Field Type Notes
ip string Exact IPv4 or IPv6 address.
limit integer 1–20 services per page.
cursor string From a previous response's next_cursor; see Pagination.
{
  "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a",
  "host": {
    "ip": "203.0.113.10",
    "location": { "country_code": "DE", "country_name": "Germany", "city": "Frankfurt am Main", "latitude": 50.1109, "longitude": 8.6821, "accuracy_radius_km": 50 },
    "network": { "asn": 64496, "organization": "Alpha Strike Labs GmbH" },
    "whois": { "organization": "Alpha Strike Labs GmbH", "netname": "ALPHASTRIKE-NET", "cidr": "203.0.113.0/24", "registry": "ripe", "country_code": "DE" },
    "reverse_dns": "host-10.example.com",
    "domains": { "items": [
      { "name": "host-10.example.com", "source": "reverse_dns" },
      { "name": "www.example.com", "source": "certificate_subject" }
    ], "has_more": false },
    "services": {
      "items": [
        { "port": 22, "protocol": "ssh", "observed_at": "2026-08-10T03:12:00Z",
          "evidence": [ { "kind": "ssh", "protocol_version": "2.0", "software_version": "OpenSSH_9.6", "host_key_algorithm": "ssh-ed25519", "host_key_fingerprint": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } ] },
        { "port": 443, "protocol": "https", "observed_at": "2026-08-10T03:14:00Z",
          "product": "F5 NGINX", "version": "1.26.1", "certificate_names": ["example.com"],
          "vulnerabilities": { "cve_count": 0, "has_exploits": false },
          "evidence": [ { "kind": "http", "status_code": 200, "server": "nginx" } ] }
      ],
      "has_more": true
    }
  },
  "next_cursor": "eyJvIjoyLCJpcCI6IjIwMy4wLjExMy4xMCJ9"
}

host_not_found (404) means no current observation exists for that exact address — the same outcome Host investigation shows as "No stored hosts."

Errors

Every error uses the same shape:

{ "request_id": "4f2b6b7a2b6a4c6a9e6e2b7a2b6a4c6a", "code": "validation_failed", "message": "request validation failed", "violations": [ { "field": "query", "code": "invalid_organization" } ] }

violations is only ever non-empty for validation_failed, and it is how you find out which part of the request was rejected — code alone tells you the request was invalid, not why.

Top-level codes

code HTTP status Meaning Costs your allowance?
invalid_json 400 The body is not valid JSON. No
organization_required 400 Your account does not currently resolve to exactly one organization. Contact us. No
unauthorized 401 The key is missing, malformed, unknown, or revoked. No
plan_expired 402 Your plan does not currently include search access. No
validation_failed 422 The query, limit, resolution, or cursor is invalid — see violations. No
search_limit_reached 429 Your daily allowance for the current period is used up. No
user_concurrency_reached 429 You already have the maximum number of searches running at once. Wait for one to finish. No
cursor_gone 410 The search cursor is missing, expired, already used, or invalid after a restart. No
host_not_found 404 No current host exists for this exact IP (/v1/hosts/detail only). — (this route is never metered)
search_capacity_exhausted 503 The service is at capacity and declined the search before starting it. No
commercial_access_unavailable 503 Your account's plan could not be confirmed. This is on our side. No
search_source_busy 503 The stored scan data was temporarily busy. Wait a few seconds and retry. No
service_unavailable 503 A dependency the request needs is unavailable. No
audit_unavailable 503 The mandatory audit record could not be written, so the request was not run. No
search_timeout 504 The search did not finish within its time budget. Narrow the query or retry. No
facet_timeout 504 The suggested-filter groups did not finish within their time budget (/v1/search/facets only). — (this route is never metered)

As Rate limits and the daily allowance explains, the only outcome that ever costs you a search is a 200 from POST /v1/search — every code in this table is free.

Violation codes (validation_failed)

These appear in violations[].code with violations[].field set to "query" unless noted.

code Meaning
invalid_query_syntax The query does not parse as a supported expression.
unknown_field A field name is not one of the approved filters.
duplicate_field The same field appears more than once in one query.
unsupported_operator Something other than a space or uppercase AND was used to combine clauses.
invalid_port The port is missing, non-numeric, or out of range.
unsupported_protocol Not an exact value from GET /v1/search/capabilities.
invalid_organization The organization phrase is missing its quotes or is otherwise malformed.
invalid_product The product phrase is missing its quotes or is otherwise malformed.
invalid_certificate_name Not one exact DNS name, or a wildcard was used.
invalid_country Not an uppercase ISO-2 code.
invalid_cpe Not one complete, quoted CPE 2.3 URI.
invalid_cve Not the exact CVE-YYYY-NNNN form.
invalid_cvss Not an inclusive min-max range within 0.010.0.
invalid_exploit Not exactly yes or no.
invalid_cidr The network does not parse as an IPv4 CIDR.
cidr_too_broad The prefix is broader than /24.
ipv6_cidr_not_supported IPv6 CIDR search is not available yet.
invalid_domain Not a registrable domain name, or a wildcard or public-suffix-only value was used.
invalid_asn Not the standalone decimal form asn:<1..4294967295>.
query_too_broad The query names no service and matches more hosts than one search can return (POST /v1/search only — combine it with a port, protocol, or country).
unsupported_query The query does not match any supported search type.
count_not_supported This query type cannot be counted (POST /v1/search/count only — exact-IP, CIDR, and domain are not accepted).
exposure_not_supported This query type cannot be aggregated yet (POST /v1/search/exposure only — domain queries are excluded).
facets_not_supported This query type has no Suggested Filters (POST /v1/search/facets only — exact-IP, CIDR, domain, and ASN are excluded).
unsupported_resolution (field: "resolution") Not global, regional, or local (POST /v1/search/exposure only).
invalid_ip (field: "ip") Not a valid IPv4 or IPv6 address (POST /v1/hosts/detail only).
invalid / query_mismatch (field: "cursor") The cursor is malformed, or does not match the query and limit it was issued for (POST /v1/search only — a cursor problem otherwise answers 410 cursor_gone, not this).

The Troubleshooting searches table describes the same conditions in the wording the web app shows for each one.

Building a client against this API

  • Handle every code in the tables above, not just the ones you expect during development — service_unavailable, search_source_busy, and the two timeout codes are ordinary operating conditions for a bounded, best-effort search service, not edge cases.
  • Only plan_expired, organization_required, and unauthorized need a human to act. Every other non-200 response — timeouts, capacity, concurrency, the daily limit, and an expired cursor — is safe to retry: back off briefly for a 503 or 504, and wait for resets_at before retrying search_limit_reached.
  • Page with next_cursor until it is absent, and treat 410 cursor_gone as "start again from an unset cursor" rather than an error to surface to a person.
  • Read allowance from each response rather than tracking your own count; it is authoritative and already includes the request that returned it.
  • Include request_id in anything you log or report — it is the fastest way to have a specific request investigated.

A minimal pagination loop, stopping when the allowance runs out or the query is exhausted:

import requests

API = "https://api.ip.alphastrike.io"
KEY = "ipk_live_7pQ2m9XvB4hK1sR6tY0wZ3nC8dF5gJaL"

def search_all(query, limit=100):
    cursor = None
    while True:
        body = {"query": query, "limit": limit}
        if cursor:
            body["cursor"] = cursor
        response = requests.post(
            f"{API}/v1/search",
            headers={"Authorization": f"Bearer {KEY}"},
            json=body,
            timeout=30,
        )
        if response.status_code != 200:
            error = response.json()
            raise RuntimeError(f"{error['code']}: {error['message']} (request_id={error['request_id']})")

        payload = response.json()
        yield from payload["results"]

        allowance = payload.get("allowance")
        if allowance and allowance["remaining"] <= 0:
            break
        cursor = payload.get("next_cursor")
        if not cursor:
            break

for host in search_all('organization:"Alpha Strike" protocol:https'):
    print(host["ip"])

This omits retry/backoff for brevity; add it around the requests.post call for 503 and 504 per the guidance above before relying on this in production.