Skip to content

We use analytics cookies to understand how the site is used and to improve it. Privacy policy

SubAnalyzer

API documentation

Start scans and retrieve results from your code, with the same scan data available in the app.

Base URL
https://subanalyzer.com/api/v1
Endpoint paths have no trailing slash.
Authentication
Authorization: Bearer <key>
Create a key
OpenAPI document
/api/v1/openapi.json
Download
On this page

Overview

The API runs the same scans as the app and returns the results as JSON. It provides three resources: scans describe each scan of a domain, subdomains contain the findings for each hostname, and changes compare a scan with the previous scan of that domain. Each endpoint below links to its reference.

Scans

A scan of a domain, including its hostnames, findings and changes since the previous scan. Start a scan, check its progress or retrieve existing scans.

Subdomains

One record per hostname, containing the scan's findings for that hostname. Retrieve records by page or download the full scan.

Changes

Changes since the previous scan of the same domain, grouped by hostname.

Quick start

  1. Create a key

    Create an API key with both scopes in your account settings, then set it in your shell as shown. All examples on this page use that key.

    Shell
    export SUBANALYZER_KEY=sa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  2. Start a scan

    The response contains a scan object. Save its id for the following steps. If the domain has already been scanned, the API returns the existing scan. Send "force": true to request a new scan. If a scan is already running, the API returns that scan even when force is true.

    Request
    curl "https://subanalyzer.com/api/v1/scans" \  -H "Authorization: Bearer $SUBANALYZER_KEY" \  -H "Content-Type: application/json" \  -d '{"domain": "example.com", "force": false}'
    Response 201
    {  "id": "7a2d9f4b-c3e1-4b8a-9d6f-2e5c8b1a3f70",  "domain": "example.com",  "status": "running",  "source": "user",  "progress": 35,  "current_phase": "deep",  "expected": {    "total_seconds": 78,    "phases": {      "light": 4.1,      "deep": 38.5,      "ip": 0.2,      "ports": 29.7,      "vuln": 2.4    },    "source": "domain"  },  "created_at": "2026-09-01T11:02:40Z",  "completed_at": null,  "subdomain_count": 0,  "active_subdomain_count": 0,  "screenshot_count": 0,  "cloud_provider_counts": {},  "port_counts": {},  "takeover_count": 0,  "takeover_checked": null,  "previous_scan_id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",  "change_deltas": null,  "change_counts": {},  "total_changes": 0,  "is_latest": true}
  3. Wait for the scan to finish

    Poll every two seconds until status is completed, and stop if the scan fails. Most scans finish within a few minutes.

    Request
    curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
    Response 200
    {  "id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",  "domain": "example.com",  "status": "completed",  "source": "user",  "progress": 100,  "current_phase": null,  "expected": null,  "created_at": "2026-09-01T09:14:07Z",  "completed_at": "2026-09-01T09:18:52Z",  "subdomain_count": 142,  "active_subdomain_count": 97,  "screenshot_count": 88,  "cloud_provider_counts": {    "AWS": 41,    "Cloudflare": 23,    "Azure": 6  },  "port_counts": {    "80": 91,    "443": 96,    "8443": 4  },  "takeover_count": 1,  "takeover_checked": true,  "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",  "change_deltas": {    "subdomains": 3,    "services": 5,    "cloud_providers": 0,    "reachable": 2,    "screenshots": 4  },  "change_counts": {    "new": 3,    "ports": 2,    "takeover": 1  },  "total_changes": 6,  "is_latest": true}
  4. Read the results

    Results are paginated. Follow next until it is null, or download the full scan as a single file.

    Request
    curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID/subdomains" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
    Response 200
    {  "next": "https://subanalyzer.com/api/v1/scans/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/subdomains?cursor=cD1hcGkuZXhhbXBsZS5jb20",  "previous": null,  "results": [    {      "subdomain": "api.example.com",      "created_at": "2026-03-14T02:11:39Z",      "cloud_provider": "AWS",      "org_name": "Amazon.com, Inc.",      "asn": 16509,      "owner_domains": [        "amazon.com"      ],      "cname": "d1f2e3a4b5c6d7.cloudfront.net",      "takeover_check": null,      "ips": [        {          "ip": "13.32.101.24"        },        {          "ip": "13.32.101.57"        }      ],      "ports": [        {          "port": 80,          "protocol": "tcp",          "status": "open"        },        {          "port": 443,          "protocol": "tcp",          "status": "open"        }      ],      "screenshots": [        {          "screenshot_url": "https://cdn.subanalyzer.com/screenshots/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/api.example.com-443.png",          "http_status_code": 200,          "page_title": "Example API",          "port_number": 443,          "created_at": "2026-09-01T09:19:31Z"        }      ]    }  ]}
  5. Check what changed

    Each subsequent scan is compared with the previous scan of the same domain. The response includes one row per changed hostname, with the old and new field values. Use this endpoint to track changes in scheduled scans.

    Request
    curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID/changes" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
    Response 200
    {  "next": "https://subanalyzer.com/api/v1/scans/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/changes?cursor=cD1zdGFnaW5nLmV4YW1wbGUuY29t",  "previous": null,  "results": [    {      "subdomain": "staging.example.com",      "change_types": [        "cname",        "takeover"      ],      "changes": {        "cname": {          "old": "staging-site.s3-website-eu-west-1.amazonaws.com",          "new": "staging-example.azurewebsites.net"        },        "takeover_check": {          "old": null,          "new": "Azure Web Apps"        }      }    }  ],  "summary": {    "scan_id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",    "scan_created_at": "2026-09-01T09:14:07Z",    "source": "user",    "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",    "previous_scan_at": "2026-08-25T04:02:11Z",    "is_baseline": false,    "has_changes": true,    "total_changes": 6,    "counts": {      "new": 3,      "ports": 2,      "takeover": 1    },    "deltas": {      "subdomains": 3,      "services": 5,      "cloud_providers": 0,      "reachable": 2,      "screenshots": 4    }  }}

Authentication

Include an API key in the Authorization header of every request. Create keys in your account settings. Each key is shown only once, when you create it. Store it securely like a password, and use it only in server-side code or scripts, never in browser-side code.

Each key has one or both scopes: read allows GET requests, and scan allows you to start scans. API access requires a paid plan. Requests return PLAN_REQUIRED while the account has no active subscription. The key works again when the subscription becomes active.

Header
Authorization: Bearer sa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Conventions

  • The base URL is https://subanalyzer.com/api/v1. Endpoint paths have no trailing slash.
  • Requests and responses use JSON. Exports are available as JSON or CSV files.
  • Timestamps use ISO 8601 in UTC and end in Z. IDs are UUIDs.
  • Scans are shared. You can retrieve any scan by ID, regardless of who started it. Scans you start through the API also appear on your dashboard.
  • If a scan is already running for a domain, another request to start a scan returns the running scan. Read requests do not modify data.
  • Objects may gain new fields, and lists may gain new values. See Versioning for compatibility guarantees.

Pagination

List responses contain results and a next link. On the last page, next is null. Follow each link exactly as returned and do not construct or modify its opaque cursor. The default page size is 100 rows. Use ?limit= to request up to 200 rows per page. Use the export endpoint to download the full scan in one request.

Response
{  "next": "https://subanalyzer.com/api/v1/scans/…/subdomains?cursor=cD1hcGkuZXhhbXBsZS5jb20",  "previous": null,  "results": [ … ]}

Rate limits

Each API key allows 5,000 requests a day, and all of an account's keys together allow 25,000. The limits reset at midnight in the Europe/Oslo time zone. Response headers show your limit, remaining requests and reset time. A limit is only lowered with at least 30 days' notice in the changelog.

  • X-RateLimit-Limitinteger

    Daily request limit for this key.

  • X-RateLimit-Remaininginteger

    Requests remaining today after this request.

  • X-RateLimit-Resetunix time

    When the daily request limit resets.

  • Retry-Afterseconds

    Included in every 429 response. Indicates how long to wait before retrying.

The API uses the same scan limits as the app: up to 5 scans running at once per account and up to 50 scans started per day. Exceeding either limit returns 429 with a specific error code. The daily scan limit is a safeguard against runaway scripts and resets at the same time as the request limit. Downloads are limited too, to 100 a day per account and a few streaming at once per server.

Headers
HTTP/1.1 200 OKContent-Type: application/jsonX-RateLimit-Limit: 5000X-RateLimit-Remaining: 4987X-RateLimit-Reset: 1788300000

Errors

Every error response is a JSON object with a human-readable detail and a machine-readable code. Handle errors by HTTP status first, then by code. Do not rely on the wording of detail. Some HTTP statuses cover multiple error codes.

  • 400
    INVALID_REQUEST

    The request failed validation. Check detail and correct the request before retrying.

  • 401
    INVALID_API_KEY

    The API key is missing, malformed, unknown or deleted. Check the Authorization header.

  • 403
    PLAN_REQUIRED

    The API key is valid, but the account has no active subscription. The key works again when the subscription becomes active.

  • 403
    SCOPE_REQUIRED

    The API key does not have the required scope. Create a key with the scope this endpoint requires.

  • 404
    NOT_FOUND

    Scan or domain not found.

  • 429
    TOO_MANY_RUNNING_SCANS

    Your account already has 5 scans running. Wait for a scan to finish before starting another.

  • 429
    PAID_SCAN_LIMIT_REACHED

    50 scans were already started today. Try again tomorrow.

  • 429
    API_LIMIT_REACHED

    This key has reached its daily limit of 5,000 requests, or the account's keys have reached their combined limit of 25,000. Check Retry-After to see when you can retry.

  • 429
    EXPORT_LIMIT_REACHED

    The account has downloaded 100 scans today. Try again tomorrow.

  • 429
    EXPORTS_BUSY

    The server is streaming as many downloads as it allows at once. Try again in a minute.

Response
{  "detail": "Invalid API key.",  "code": "INVALID_API_KEY"}

Versioning

The current API version is 1, included in every URL as /api/v1. Changes within a version are additive. Changes that would break clients following the guidelines below require a new version. The previous version remains available for at least six months after the new version is released.

Two things can change without a new version. A rate limit can go down, announced in the changelog at least 30 days before it applies. A security fix can ship at once, listed in the changelog the same day.

May be added at any time

  • New endpoints
  • New fields on any object
  • New optional parameters
  • New values in source, change_types, takeover_check and cloud_provider
  • New error codes under existing HTTP statuses

Require a new version

  • Removing or renaming a field, parameter, endpoint or operation name
  • Changing a field's type or meaning
  • Changing a URL or the structure of a paginated response
  • Rejecting a request that v1 currently accepts

Build a compatible client

  • Read object fields by key and ignore unknown fields.
  • Allow for new values in source, change_types, takeover_check and cloud_provider. Provide a fallback for values your client does not recognize.
  • Handle errors by HTTP status first, then by code. Do not rely on detail.
  • Follow next exactly as returned. Do not construct cursors.
  • If you generate a client from the OpenAPI document, its operation names (start_scan, get_scan and the rest) stay for all of v1.

How changes are announced

  • The changelog below lists each change and its date. Subscribe to the Atom feed to receive updates.

  • Accounts with an API key receive an email announcing a new version at least six months before the previous version is retired.

  • When v1 is replaced, its responses include Deprecation and Sunset headers with the relevant dates, plus a Link to this section, as described in RFC 9745 and RFC 8594. See the example for these headers. They are not sent while v1 is current.

  • A feature deprecated within a version is marked as deprecated in the OpenAPI document and on this page, and recorded in the changelog. It keeps working until a new version replaces the old one.

Example
HTTP/1.1 200 OKContent-Type: application/jsonDeprecation: @1798761600Sunset: Thu, 01 Jul 2027 00:00:00 GMTLink: <https://subanalyzer.com/docs/api#versioning>; rel="deprecation"; type="text/html",  <https://subanalyzer.com/docs/api#versioning>; rel="sunset"; type="text/html"

Changelog

Changes to v1, newest first, with any action required from API clients. Subscribe to the feed for updates.

    • Added

      Two exceptions to the versioning policy: a lower rate limit is announced here at least 30 days before it applies, and a security fix can ship at once, listed here the same day.

    • Added

      A ceiling of 25,000 requests a day per account across its keys, beside the 5,000 per key: X-RateLimit-Remaining reports the smaller of the two. No account within its keys' limits meets it.

    • Added

      Downloads are capped at 100 a day per account (429 EXPORT_LIMIT_REACHED), and a server streams a few at once (429 EXPORTS_BUSY, Retry-After: 60).

    • Changed

      Every operation has a name of its own: start_scan, get_scan, get_latest_scan, list_domain_scans, list_subdomains, list_changes and download_scan. A client generated from the document gets them as its method names, and they stay for all of v1.

    • Changed

      source and change_types list their values as x-extensible-enum in place of enum: both can gain a value inside v1, so keep a default for one you have not seen.

    • Changed

      changes on a change row is typed: ips, open_ports, cname, cloud_provider, owner and takeover_check, each with old and new. The rows are the same as before.

    • Fixed

      An empty side of cname, cloud_provider, takeover_check and owner's org_name in changes is always null. It was sometimes an empty string.

    • Fixed

      source no longer lists refresh, which no scan carries, and its description explains asn.

    • Fixed

      A cursor that was not taken from a next or previous link answers 404 NOT_FOUND. Some such cursors were a server error.

    • Fixed

      A port entry on a subdomain row carries port, protocol and status only. Some carried an undocumented carried key.

    • Added

      This changelog, inside the OpenAPI document as info.x-changelog, on the docs page and as an Atom feed at /docs/api/changelog.xml.

    • Added

      The headers v1 will answer with once a newer version has replaced it: Deprecation and Sunset with the dates, and a Link to the docs. None are sent while v1 is current.

    • Changed

      takeover_check withholds NS Delegation findings again while that check is reworked. They had been reported since 2026-10-05.

    • Changed

      source on the scan object can be related: a related domain of a scanned one, scanned in the background.

    • Added

      takeover_check can report Unregistered NS: the hostname is delegated to a nameserver on a domain nobody has registered.

    • Changed

      takeover_check reports NS Delegation again, for a hostname whose delegation points at nameservers nobody holds.

    • Changed

      At most 5 scans run at once per account, down from 10. One more answers 429 TOO_MANY_RUNNING_SCANS.

    • Changed

      expected on a running scan rests on measured runs of the current pipeline, and expected.phases holds average seconds per phase.

    • Fixed

      A scan's changes leave out a hostname whose only change was a withheld finding, so the rows add up to summary.total_changes.

    • Changed

      takeover_check withholds NS Delegation findings until the check can tell a claimable name from a held one.

    • Added

      Version 1: start a scan, read a scan, read a domain's latest scan and its history, list a scan's subdomains and its changes, download a scan, and this document at /api/v1/openapi.json.

Scans

A scan of a domain, including its hostnames, findings and changes since the previous scan. Start a scan, check its progress or retrieve existing scans.

The scan object

A single scan of a domain. While the scan is pending or running, progress, current_phase and expected describe its progress and estimated duration. Once it stops, the counts describe its findings and the change fields compare them with the previous scan of the same domain.

Returned by Start a scan, Get a scan, Get a domain's latest scan and List a domain's scans.

Fields

  • iduuid

    The scan ID.

  • domainstring
  • statusenum

    One of pending, running, completed, failed.

  • sourceenum

    What triggered the scan: a person (user), scheduled monitoring (monitor) or background scanning of a domain related to another scanned domain (related). Scans older than 2026-08-16 can also carry asn, a scan type the site no longer starts. Known values: user, monitor, related, asn. A version can add to this list, so keep a default for a value you have not seen.

  • progressinteger

    Progress from 0 to 100 while running, and 100 when complete.

  • current_phaseenum or null

    The current scan phase, or null once the scan has stopped. One of light, deep, ip, ports, vuln.

  • expectedobject or null

    The estimated scan duration. Null once the scan has stopped.

    Show child fields
    • total_secondsnumber

      The estimated total scan duration in seconds.

    • phasesmap of number

      Average duration in seconds for each phase, keyed by phase name.

    • sourceenum

      Whether the estimate uses the domain's scan history or site-wide data for domains with a similar number of discovered subdomains. One of domain, global.

  • created_attimestamp
  • completed_attimestamp or null
  • subdomain_countinteger

    Number of hostnames in the scan.

  • active_subdomain_countinteger

    Number of hostnames that responded on a web port.

  • screenshot_countinteger
  • cloud_provider_countsmap of integer

    Number of hostnames per cloud provider.

  • port_countsmap of integer

    Number of hostnames per open port.

  • takeover_countinteger

    Number of hostnames with a takeover risk finding.

  • takeover_checkedboolean or null

    Whether the takeover check stage reported results. False means the scan ended without those results, so a zero count does not rule out takeover risks. Null for scans created before this field was introduced.

  • previous_scan_iduuid or null

    The ID of the scan used for comparison. Null for a baseline scan.

  • change_deltasobject or null

    Net changes since the previous scan. Null for a baseline scan.

    Show child fields
    • subdomainsinteger or null
    • servicesinteger or null
    • cloud_providersinteger or null
    • reachableinteger or null
    • screenshotsinteger or null
  • change_countsmap of integer

    Number of changed hostnames per change type. Empty for a baseline scan.

  • total_changesinteger
  • is_latestboolean

    Whether this is the scan currently displayed for the domain.

The scan object
{  "id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",  "domain": "example.com",  "status": "completed",  "source": "user",  "progress": 100,  "current_phase": null,  "expected": null,  "created_at": "2026-09-01T09:14:07Z",  "completed_at": "2026-09-01T09:18:52Z",  "subdomain_count": 142,  "active_subdomain_count": 97,  "screenshot_count": 88,  "cloud_provider_counts": {    "AWS": 41,    "Cloudflare": 23,    "Azure": 6  },  "port_counts": {    "80": 91,    "443": 96,    "8443": 4  },  "takeover_count": 1,  "takeover_checked": true,  "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",  "change_deltas": {    "subdomains": 3,    "services": 5,    "cloud_providers": 0,    "reachable": 2,    "screenshots": 4  },  "change_counts": {    "new": 3,    "ports": 2,    "takeover": 1  },  "total_changes": 6,  "is_latest": true}

Start a scan

POST/api/v1/scans

Starts a domain scan or returns an existing scan. If the domain has already been scanned, a new scan starts only when force is true. If a scan is already running, that scan is returned even when force is true. Scans started through the API appear on your dashboard.

Body

  • domainstringrequired

    The domain to scan, for example example.com.

  • forcebooleandefault false

    Request a new scan even if the domain has already been scanned. Without this option, the API returns the existing scan. A scan already running is returned regardless of this setting.

Response

  • 201A new scan was started. Returns a scan object.
  • 200An existing scan was returned: the latest completed scan or a scan currently running. Returns a scan object.

Errors

  • 400The request failed validation.
  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED), the account already has 5 scans running (TOO_MANY_RUNNING_SCANS) or 50 scans were already started today (PAID_SCAN_LIMIT_REACHED). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/scans" \  -H "Authorization: Bearer $SUBANALYZER_KEY" \  -H "Content-Type: application/json" \  -d '{"domain": "example.com", "force": false}'
Response
{  "id": "7a2d9f4b-c3e1-4b8a-9d6f-2e5c8b1a3f70",  "domain": "example.com",  "status": "running",  "source": "user",  "progress": 35,  "current_phase": "deep",  "expected": {    "total_seconds": 78,    "phases": {      "light": 4.1,      "deep": 38.5,      "ip": 0.2,      "ports": 29.7,      "vuln": 2.4    },    "source": "domain"  },  "created_at": "2026-09-01T11:02:40Z",  "completed_at": null,  "subdomain_count": 0,  "active_subdomain_count": 0,  "screenshot_count": 0,  "cloud_provider_counts": {},  "port_counts": {},  "takeover_count": 0,  "takeover_checked": null,  "previous_scan_id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",  "change_deltas": null,  "change_counts": {},  "total_changes": 0,  "is_latest": true}

Get a scan

GET/api/v1/scans/{scan_id}

Returns a scan and its summary. Poll this endpoint while status is pending or running. You can retrieve any scan, regardless of who started it.

Path parameters

  • scan_idstringrequired

    The scan ID.

Response

Errors

  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 404Scan not found.
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
Response
{  "id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",  "domain": "example.com",  "status": "completed",  "source": "user",  "progress": 100,  "current_phase": null,  "expected": null,  "created_at": "2026-09-01T09:14:07Z",  "completed_at": "2026-09-01T09:18:52Z",  "subdomain_count": 142,  "active_subdomain_count": 97,  "screenshot_count": 88,  "cloud_provider_counts": {    "AWS": 41,    "Cloudflare": 23,    "Azure": 6  },  "port_counts": {    "80": 91,    "443": 96,    "8443": 4  },  "takeover_count": 1,  "takeover_checked": true,  "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",  "change_deltas": {    "subdomains": 3,    "services": 5,    "cloud_providers": 0,    "reachable": 2,    "screenshots": 4  },  "change_counts": {    "new": 3,    "ports": 2,    "takeover": 1  },  "total_changes": 6,  "is_latest": true}

Get a domain's latest scan

GET/api/v1/domains/{domain}

Returns the domain's latest scan that did not fail, without starting a new scan. Returns 404 if the domain has not been scanned.

Path parameters

  • domainstringrequired

    A domain, for example example.com.

Response

Errors

  • 400The request failed validation.
  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 404No scan found for this domain.
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/domains/example.com" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
Response
{  "id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",  "domain": "example.com",  "status": "completed",  "source": "user",  "progress": 100,  "current_phase": null,  "expected": null,  "created_at": "2026-09-01T09:14:07Z",  "completed_at": "2026-09-01T09:18:52Z",  "subdomain_count": 142,  "active_subdomain_count": 97,  "screenshot_count": 88,  "cloud_provider_counts": {    "AWS": 41,    "Cloudflare": 23,    "Azure": 6  },  "port_counts": {    "80": 91,    "443": 96,    "8443": 4  },  "takeover_count": 1,  "takeover_checked": true,  "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",  "change_deltas": {    "subdomains": 3,    "services": 5,    "cloud_providers": 0,    "reachable": 2,    "screenshots": 4  },  "change_counts": {    "new": 3,    "ports": 2,    "takeover": 1  },  "total_changes": 6,  "is_latest": true}

List a domain's scans

GET/api/v1/domains/{domain}/scans

Lists all scans of the domain, newest first, including failed scans. Each scan includes a summary of changes since the previous scan. Returns an empty page if the domain has not been scanned.

Path parameters

  • domainstringrequired

    A domain, for example example.com.

Query parameters

  • cursorstring

    The position to resume from, provided in the previous page's next link. Do not construct this value yourself.

  • limitinteger

    Number of rows per page, from 1 to 200. Defaults to 100.

Response

  • nexturl or null

    Link to the next page, or null if there are no more pages.

  • previousurl or null
  • resultsarray of scan objects

Errors

  • 400The request failed validation.
  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/domains/example.com/scans" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
Response
{  "next": "https://subanalyzer.com/api/v1/domains/example.com/scans?cursor=cD0yMDI2LTA4LTI1",  "previous": null,  "results": [    {      "id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",      "domain": "example.com",      "status": "completed",      "source": "user",      "progress": 100,      "current_phase": null,      "expected": null,      "created_at": "2026-09-01T09:14:07Z",      "completed_at": "2026-09-01T09:18:52Z",      "subdomain_count": 142,      "active_subdomain_count": 97,      "screenshot_count": 88,      "cloud_provider_counts": {        "AWS": 41,        "Cloudflare": 23,        "Azure": 6      },      "port_counts": {        "80": 91,        "443": 96,        "8443": 4      },      "takeover_count": 1,      "takeover_checked": true,      "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",      "change_deltas": {        "subdomains": 3,        "services": 5,        "cloud_providers": 0,        "reachable": 2,        "screenshots": 4      },      "change_counts": {        "new": 3,        "ports": 2,        "takeover": 1      },      "total_changes": 6,      "is_latest": true    },    {      "id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",      "domain": "example.com",      "status": "completed",      "source": "user",      "progress": 100,      "current_phase": null,      "expected": null,      "created_at": "2026-08-25T04:02:11Z",      "completed_at": "2026-08-25T04:06:37Z",      "subdomain_count": 139,      "active_subdomain_count": 95,      "screenshot_count": 84,      "cloud_provider_counts": {        "AWS": 41,        "Cloudflare": 22,        "Azure": 6      },      "port_counts": {        "80": 90,        "443": 94,        "8443": 4      },      "takeover_count": 0,      "takeover_checked": true,      "previous_scan_id": null,      "change_deltas": null,      "change_counts": {},      "total_changes": 0,      "is_latest": false    }  ]}

Subdomains

One record per hostname, containing the scan's findings for that hostname. Retrieve records by page or download the full scan.

The subdomain object

A hostname found in a scan, with its resolved addresses, the organizations that own those addresses, responding ports and screenshots.

Returned by List a scan's subdomains and Download a scan.

Fields

  • subdomainstring

    The hostname.

  • created_attimestamp

    When the hostname was first seen.

  • cloud_providerstring or null
  • org_namestring or null

    The organization the address is registered to.

  • asninteger or null
  • owner_domainsarray of strings
  • cnamestring or null
  • takeover_checkstring or null

    The service associated with a potential takeover risk, such as AWS S3 or Azure Web Apps. Null when no takeover risk was found.

  • ipsarray of objects

    Empty if the hostname no longer resolves.

    Show child fields
    • ipstring
  • portsarray of objects
    Show child fields
    • portinteger
    • protocolstring
    • statusstring
  • screenshotsarray of objects
    Show child fields
    • screenshot_urlurl

      Absolute screenshot URL hosted on cdn.subanalyzer.com.

    • http_status_codeinteger or null
    • page_titlestring or null
    • port_numberinteger
    • created_attimestamp

      When the screenshot was captured.

The subdomain object
{  "subdomain": "api.example.com",  "created_at": "2026-03-14T02:11:39Z",  "cloud_provider": "AWS",  "org_name": "Amazon.com, Inc.",  "asn": 16509,  "owner_domains": [    "amazon.com"  ],  "cname": "d1f2e3a4b5c6d7.cloudfront.net",  "takeover_check": null,  "ips": [    {      "ip": "13.32.101.24"    },    {      "ip": "13.32.101.57"    }  ],  "ports": [    {      "port": 80,      "protocol": "tcp",      "status": "open"    },    {      "port": 443,      "protocol": "tcp",      "status": "open"    }  ],  "screenshots": [    {      "screenshot_url": "https://cdn.subanalyzer.com/screenshots/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/api.example.com-443.png",      "http_status_code": 200,      "page_title": "Example API",      "port_number": 443,      "created_at": "2026-09-01T09:19:31Z"    }  ]}

List a scan's subdomains

GET/api/v1/scans/{scan_id}/subdomains

Lists the scan's hostnames and their addresses, open ports, cloud provider, organization, CNAME, takeover risk finding and screenshots. Hostnames that no longer resolve are included with no addresses. Returns the same records as the export, sorted by hostname.

Path parameters

  • scan_idstringrequired

    The scan ID.

Query parameters

  • cursorstring

    The position to resume from, provided in the previous page's next link. Do not construct this value yourself.

  • limitinteger

    Number of rows per page, from 1 to 200. Defaults to 100.

Response

  • nexturl or null

    Link to the next page, or null if there are no more pages.

  • previousurl or null
  • resultsarray of subdomain objects

Errors

  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 404Scan not found.
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID/subdomains" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
Response
{  "next": "https://subanalyzer.com/api/v1/scans/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/subdomains?cursor=cD1hcGkuZXhhbXBsZS5jb20",  "previous": null,  "results": [    {      "subdomain": "api.example.com",      "created_at": "2026-03-14T02:11:39Z",      "cloud_provider": "AWS",      "org_name": "Amazon.com, Inc.",      "asn": 16509,      "owner_domains": [        "amazon.com"      ],      "cname": "d1f2e3a4b5c6d7.cloudfront.net",      "takeover_check": null,      "ips": [        {          "ip": "13.32.101.24"        },        {          "ip": "13.32.101.57"        }      ],      "ports": [        {          "port": 80,          "protocol": "tcp",          "status": "open"        },        {          "port": 443,          "protocol": "tcp",          "status": "open"        }      ],      "screenshots": [        {          "screenshot_url": "https://cdn.subanalyzer.com/screenshots/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/api.example.com-443.png",          "http_status_code": 200,          "page_title": "Example API",          "port_number": 443,          "created_at": "2026-09-01T09:19:31Z"        }      ]    }  ]}

Download a scan

GET/api/v1/scans/{scan_id}/export

Streams the full scan as a single JSON or CSV file, using the same records as the subdomains endpoint. CSV exports use one column per field, with multiple values separated by semicolons.

Path parameters

  • scan_idstringrequired

    The scan ID.

Query parameters

  • formatstringrequired

Response

Errors

  • 400The request failed validation.
  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 404Scan not found.
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED), the account has already downloaded 100 exports today (EXPORT_LIMIT_REACHED) or the server is already streaming as many exports as it allows (EXPORTS_BUSY). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID/export?format=csv" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
Response
[  [    {      "subdomain": "api.example.com",      "created_at": "2026-03-14T02:11:39Z",      "cloud_provider": "AWS",      "org_name": "Amazon.com, Inc.",      "asn": 16509,      "owner_domains": [        "amazon.com"      ],      "cname": "d1f2e3a4b5c6d7.cloudfront.net",      "takeover_check": null,      "ips": [        {          "ip": "13.32.101.24"        },        {          "ip": "13.32.101.57"        }      ],      "ports": [        {          "port": 80,          "protocol": "tcp",          "status": "open"        },        {          "port": 443,          "protocol": "tcp",          "status": "open"        }      ],      "screenshots": [        {          "screenshot_url": "https://cdn.subanalyzer.com/screenshots/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/api.example.com-443.png",          "http_status_code": 200,          "page_title": "Example API",          "port_number": 443,          "created_at": "2026-09-01T09:19:31Z"        }      ]    }  ]]

Changes

Changes since the previous scan of the same domain, grouped by hostname.

The change object

A hostname with changes since the previous scan, including the change types and the old and new values of each changed field.

Returned by List a scan's changes.

Fields

  • subdomainstring

    The hostname that changed.

  • change_typesarray of enums

    Known values: new, removed, resolved, cname, addresses, owner, cloud, ports, takeover. A version can add to this list, so keep a default for a value you have not seen.

  • changesobject

    Old and new values for each changed field.

    Show child fields
    • ipsobject

      The addresses the hostname resolves to.

      Show child fields
      • oldarray of strings or null

        Null on a hostname the previous scan did not have.

      • newarray of strings

        Empty on a hostname that stopped resolving.

    • open_portsobject

      The open ports.

      Show child fields
      • oldarray of integers
      • newarray of integers
    • cnameobject

      The CNAME target.

      Show child fields
      • oldstring or null

        Null where there was none.

      • newstring or null

        Null where there is none now.

    • cloud_providerobject

      The cloud provider.

      Show child fields
      • oldstring or null

        Null where there was none.

      • newstring or null

        Null where there is none now.

    • ownerobject

      The organization and AS number the addresses are registered to.

      Show child fields
      • oldobject
        Show child fields
        • org_namestring or null
        • asninteger or null
      • newobject
        Show child fields
        • org_namestring or null
        • asninteger or null
    • takeover_checkobject

      The takeover finding, as the service name.

      Show child fields
      • oldstring or null

        Null where there was none.

      • newstring or null

        Null where there is none now.

The change object
{  "subdomain": "staging.example.com",  "change_types": [    "cname",    "takeover"  ],  "changes": {    "cname": {      "old": "staging-site.s3-website-eu-west-1.amazonaws.com",      "new": "staging-example.azurewebsites.net"    },    "takeover_check": {      "old": null,      "new": "Azure Web Apps"    }  }}

List a scan's changes

GET/api/v1/scans/{scan_id}/changes

Returns changes since the previous scan of the domain: one row per changed hostname, with the old and new field values and an overall change summary. A baseline scan returns an empty page with summary.is_baseline set to true.

Path parameters

  • scan_idstringrequired

    The scan ID.

Query parameters

  • cursorstring

    The position to resume from, provided in the previous page's next link. Do not construct this value yourself.

  • limitinteger

    Number of rows per page, from 1 to 200. Defaults to 100.

Response

  • nexturl or null

    Link to the next page, or null if there are no more pages.

  • previousurl or null
  • resultsarray of change objects
  • summaryobject

    Overall change summary, included on every page.

    Show child fields
    • scan_iduuid
    • scan_created_attimestamp
    • sourceenum

      Known values: user, monitor, related, asn. A version can add to this list, so keep a default for a value you have not seen.

    • previous_scan_iduuid or null
    • previous_scan_attimestamp or null
    • is_baselineboolean

      True when no previous scan was available for comparison.

    • has_changesboolean
    • total_changesinteger
    • countsmap of integer

      Number of changed hostnames per change type.

    • deltasobject
      Show child fields
      • subdomainsinteger or null
      • servicesinteger or null
      • cloud_providersinteger or null
      • reachableinteger or null
      • screenshotsinteger or null

Errors

  • 401Missing or invalid API key (INVALID_API_KEY).
  • 403The API key is valid, but the account has no active subscription (PLAN_REQUIRED) or the key lacks the required scope (SCOPE_REQUIRED).
  • 404Scan not found.
  • 429The key or the account has reached its daily request limit (API_LIMIT_REACHED). Check Retry-After to see when you can retry.

The name in parentheses is the response body's code. See Errors for the full list.

Request
curl "https://subanalyzer.com/api/v1/scans/$SCAN_ID/changes" \  -H "Authorization: Bearer $SUBANALYZER_KEY"
Response
{  "next": "https://subanalyzer.com/api/v1/scans/3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/changes?cursor=cD1zdGFnaW5nLmV4YW1wbGUuY29t",  "previous": null,  "results": [    {      "subdomain": "staging.example.com",      "change_types": [        "cname",        "takeover"      ],      "changes": {        "cname": {          "old": "staging-site.s3-website-eu-west-1.amazonaws.com",          "new": "staging-example.azurewebsites.net"        },        "takeover_check": {          "old": null,          "new": "Azure Web Apps"        }      }    }  ],  "summary": {    "scan_id": "3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42",    "scan_created_at": "2026-09-01T09:14:07Z",    "source": "user",    "previous_scan_id": "b91e6d3a-2c7f-4e8b-a1d4-6f0c3e9b2a17",    "previous_scan_at": "2026-08-25T04:02:11Z",    "is_baseline": false,    "has_changes": true,    "total_changes": 6,    "counts": {      "new": 3,      "ports": 2,      "takeover": 1    },    "deltas": {      "subdomains": 3,      "services": 5,      "cloud_providers": 0,      "reachable": 2,      "screenshots": 4    }  }}