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.
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
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.
Shellexport SUBANALYZER_KEY=sa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Start a scan
The response contains a scan object. Save its
idfor the following steps. If the domain has already been scanned, the API returns the existing scan. Send"force": trueto request a new scan. If a scan is already running, the API returns that scan even whenforceis true.Requestcurl "https://subanalyzer. com/ api/ v1/ scans" \ -H "Authorization: Bearer $SUBANALYZER_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "example. com", "force": false}' import osimport requests response =requests. post( "https:// subanalyzer. com/ api/ v1/ scans", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"}, json= {"domain": "example. com", "force": False},)response. raise_for_status()scan = response. json() const response =await fetch("https:// subanalyzer. com/ api/ v1/ scans", { method: "POST", headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}`, "Content-Type": "application/json", }, body: JSON. stringify({ domain: "example. com", force: false }),});const scan = await response. json(); 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} Wait for the scan to finish
Poll every two seconds until
statusiscompleted, and stop if the scan fails. Most scans finish within a few minutes.Requestcurl "https://subanalyzer. com/ api/ v1/ scans/ $SCAN_ID" \ -H "Authorization: Bearer $SUBANALYZER_KEY" import osimport requests response =requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()scan = response. json() const response =await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const scan = await response. json(); 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} Read the results
Results are paginated. Follow
nextuntil it isnull, or download the full scan as a single file.Requestcurl "https://subanalyzer. com/ api/ v1/ scans/ $SCAN_ID/ subdomains" \ -H "Authorization: Bearer $SUBANALYZER_KEY" import osimport requests response =requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}/ subdomains", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()page = response. json() const response =await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}/ subdomains`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const page = await response. json(); 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" } ] } ]} 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.
Requestcurl "https://subanalyzer. com/ api/ v1/ scans/ $SCAN_ID/ changes" \ -H "Authorization: Bearer $SUBANALYZER_KEY" import osimport requests response =requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}/ changes", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()page = response. json() const response =await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}/ changes`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const page = await response. json(); 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.
Authorization: Bearer sa_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxConventions
- 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.
{ "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.
HTTP/1. 1 200 OKContent-Type: application/jsonX-RateLimit-Limit: 5000X-RateLimit-Remaining: 4987X-RateLimit-Reset: 1788300000HTTP/1. 1 429 Too Many RequestsContent-Type: application/jsonRetry-After: 45960X-RateLimit-Limit: 5000X-RateLimit-Remaining: 0X-RateLimit-Reset: 1788300000Errors
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_REQUESTThe request failed validation. Check
detailand correct the request before retrying. - 401
INVALID_API_KEYThe API key is missing, malformed, unknown or deleted. Check the
Authorizationheader. - 403
PLAN_REQUIREDThe API key is valid, but the account has no active subscription. The key works again when the subscription becomes active.
- 403
SCOPE_REQUIREDThe API key does not have the required scope. Create a key with the scope this endpoint requires.
- 404
NOT_FOUNDScan or domain not found.
- 429
TOO_MANY_RUNNING_SCANSYour account already has 5 scans running. Wait for a scan to finish before starting another.
- 429
PAID_SCAN_LIMIT_REACHED50 scans were already started today. Try again tomorrow.
- 429
API_LIMIT_REACHEDThis 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-Afterto see when you can retry. - 429
EXPORT_LIMIT_REACHEDThe account has downloaded 100 scans today. Try again tomorrow.
- 429
EXPORTS_BUSYThe server is streaming as many downloads as it allows at once. Try again in a minute.
{ "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_checkandcloud_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_checkandcloud_provider. Provide a fallback for values your client does not recognize. - Handle errors by HTTP status first, then by
code. Do not rely ondetail. - Follow
nextexactly as returned. Do not construct cursors. - If you generate a client from the OpenAPI document, its operation names (
start_scan,get_scanand 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
DeprecationandSunsetheaders with the relevant dates, plus aLinkto 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.
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-Remainingreports 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 (429EXPORTS_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_changesanddownload_scan. A client generated from the document gets them as its method names, and they stay for all of v1. - Changed
sourceandchange_typeslist their values asx-extensible-enumin place ofenum: both can gain a value inside v1, so keep a default for one you have not seen. - Changed
changeson a change row is typed:ips,open_ports,cname,cloud_provider,ownerandtakeover_check, each witholdandnew. The rows are the same as before. - Fixed
An empty side of
cname,cloud_provider,takeover_checkandowner'sorg_nameinchangesis alwaysnull. It was sometimes an empty string. - Fixed
sourceno longer listsrefresh, which no scan carries, and its description explainsasn. - Fixed
A
cursorthat was not taken from anextorpreviouslink answers 404NOT_FOUND. Some such cursors were a server error. - Fixed
A port entry on a subdomain row carries
port,protocolandstatusonly. Some carried an undocumentedcarriedkey.
- Added
- 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:
DeprecationandSunsetwith the dates, and aLinkto the docs. None are sent while v1 is current.
- Added
- Changed
takeover_checkwithholdsNS Delegationfindings again while that check is reworked. They had been reported since 2026-10-05.
- Changed
- Changed
sourceon the scan object can berelated: a related domain of a scanned one, scanned in the background.
- Changed
- Added
takeover_checkcan reportUnregistered NS: the hostname is delegated to a nameserver on a domain nobody has registered. - Changed
takeover_checkreportsNS Delegationagain, for a hostname whose delegation points at nameservers nobody holds.
- Added
- Changed
At most 5 scans run at once per account, down from 10. One more answers 429
TOO_MANY_RUNNING_SCANS. - Changed
expectedon a running scan rests on measured runs of the current pipeline, andexpected.phasesholds average seconds per phase.
- Changed
- 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.
- Fixed
- Changed
takeover_checkwithholdsNS Delegationfindings until the check can tell a claimable name from a held one.
- Changed
- 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.
- Added
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 carryasn, 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 fieldsHide 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 fieldsHide 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.
{ "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/
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.
curl "https:// subanalyzer. com/ api/ v1/ scans" \ -H "Authorization: Bearer $SUBANALYZER_KEY" \ -H "Content-Type: application/json" \ -d '{"domain": "example. com", "force": false}'import osimport requests response = requests. post( "https:// subanalyzer. com/ api/ v1/ scans", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"}, json= {"domain": "example. com", "force": False},)response. raise_for_status()scan = response. json()const response = await fetch("https:// subanalyzer. com/ api/ v1/ scans", { method: "POST", headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}`, "Content-Type": "application/json", }, body: JSON. stringify({ domain: "example. com", force: false }),});const scan = await response. json();{ "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}{ "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}{ "detail": "Enter a valid domain name. ", "code": "INVALID_REQUEST"}Get a scan
GET/
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
- 200Returns a scan object.
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.
curl "https:// subanalyzer. com/ api/ v1/ scans/ $SCAN_ID" \ -H "Authorization: Bearer $SUBANALYZER_KEY"import osimport requests response = requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()scan = response. json()const response = await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const scan = await response. json();{ "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}{ "detail": "Not found. ", "code": "NOT_FOUND"}Get a domain's latest scan
GET/
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
- 200Returns 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).
- 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.
curl "https:// subanalyzer. com/ api/ v1/ domains/ example. com" \ -H "Authorization: Bearer $SUBANALYZER_KEY"import osimport requests response = requests. get( "https:// subanalyzer. com/ api/ v1/ domains/ example. com", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()scan = response. json()const response = await fetch("https:// subanalyzer. com/ api/ v1/ domains/ example. com", { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const scan = await response. json();{ "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}{ "detail": "Enter a valid domain name. ", "code": "INVALID_REQUEST"}{ "detail": "Not found. ", "code": "NOT_FOUND"}List a domain's scans
GET/
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
- 200Returns a page of scan objects.
- 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.
curl "https:// subanalyzer. com/ api/ v1/ domains/ example. com/ scans" \ -H "Authorization: Bearer $SUBANALYZER_KEY"import osimport requests response = requests. get( "https:// subanalyzer. com/ api/ v1/ domains/ example. com/ scans", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()page = response. json()const response = await fetch("https:// subanalyzer. com/ api/ v1/ domains/ example. com/ scans", { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const page = await response. json();{ "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 } ]}{ "detail": "Enter a valid domain name. ", "code": "INVALID_REQUEST"}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 fieldsHide child fields
- ipstring
- portsarray of objects
Show child fieldsHide child fields
- portinteger
- protocolstring
- statusstring
- screenshotsarray of objects
Show child fieldsHide 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.
{ "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/
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
- 200Returns a page of subdomain objects.
- 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.
curl "https:// subanalyzer. com/ api/ v1/ scans/ $SCAN_ID/ subdomains" \ -H "Authorization: Bearer $SUBANALYZER_KEY"import osimport requests response = requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}/ subdomains", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()page = response. json()const response = await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}/ subdomains`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const page = await response. json();{ "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" } ] } ]}{ "detail": "Not found. ", "code": "NOT_FOUND"}Download a scan
GET/
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
- 200The rows as a JSON array. Returns an array of subdomain objects, or CSV.
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.
curl "https:// subanalyzer. com/ api/ v1/ scans/ $SCAN_ID/ export?format= csv" \ -H "Authorization: Bearer $SUBANALYZER_KEY"import osimport requests response = requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}/ export", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"}, params= {"format": "csv"},)response. raise_for_status()with open("example. com. csv", "wb") as file: file. write(response. content)const response = await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}/ export?format= csv`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const file = await response. text();[ [ { "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" } ] } ]]subdomain, ips, ports, cloud_provider, org_name, cname, created_at, asn, owner_domains, takeover_check, http_status, screenshot_urls, port_protocols, port_statuses, page_titles, screenshot_ports, screenshot_created_atapi. example. com, 13. 32. 101. 24;13. 32. 101. 57, 80;443, AWS, "Amazon. com, Inc. ", d1f2e3a4b5c6d7. cloudfront. net, 2026-03-14T02:11:39Z, 16509, amazon. com,, 200, https:// cdn. subanalyzer. com/ screenshots/ 3f7c2a1e-8b4d-4c6a-9e2f-1d5b7a9c0e42/ api. example. com-443. png, tcp;tcp, open;open, Example API, 443, 2026-09-01T09:19:31Z{ "detail": "Enter a valid domain name. ", "code": "INVALID_REQUEST"}{ "detail": "Not found. ", "code": "NOT_FOUND"}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 fieldsHide child fields
- ipsobject
The addresses the hostname resolves to.
Show child fieldsHide 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 fieldsHide child fields
- oldarray of integers
- newarray of integers
- cnameobject
The CNAME target.
Show child fieldsHide 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 fieldsHide 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 fieldsHide child fields
- oldobject
Show child fieldsHide child fields
- org_namestring or null
- asninteger or null
- newobject
Show child fieldsHide child fields
- org_namestring or null
- asninteger or null
- takeover_checkobject
The takeover finding, as the service name.
Show child fieldsHide child fields
- oldstring or null
Null where there was none.
- newstring or null
Null where there is none now.
{ "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/
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
- 200Returns a page of change objects.
- 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 fieldsHide 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 fieldsHide 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.
curl "https:// subanalyzer. com/ api/ v1/ scans/ $SCAN_ID/ changes" \ -H "Authorization: Bearer $SUBANALYZER_KEY"import osimport requests response = requests. get( f"https:// subanalyzer. com/ api/ v1/ scans/ {scan_id}/ changes", headers= {"Authorization": f"Bearer {os. environ['SUBANALYZER_KEY']}"},)response. raise_for_status()page = response. json()const response = await fetch(`https:// subanalyzer. com/ api/ v1/ scans/ ${scanId}/ changes`, { headers: { Authorization: `Bearer ${process. env. SUBANALYZER_KEY}` },});const page = await response. json();{ "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 } }}{ "detail": "Not found. ", "code": "NOT_FOUND"}