Query live domain intelligence for leads, market analysis, and research.
Every endpoint takes a JSON body over POST and answers with JSON, unless it delivers a file. The same call also works as a GET with the parameters in the query string, convenient for a browser, wget or a cron job, at the price of putting your token into the URL, where proxies and shell history can see it.
| Endpoint | Purpose | Access |
|---|---|---|
| /lookup | Characteristics of a single domain | any plan |
| /redirect | Domains that forward to a domain | any plan |
| /reverseip | Domains currently sharing an IP address | any plan |
| /dnshistory | How a domain’s DNS records changed over time | Basic or Premium |
| /daily | Domains discovered on one day, gzipped, plus follow-up | Basic; follow-up Premium |
| /snapshot | The complete domain list, gzipped, names or enriched | List / Data Export |
| Content-Type | application/json |
| Accept | application/json |
| Accept-Encoding | gzip (recommended, responses can be large) |
There is no separate auth header. Pass your token in the body of every request. You find it in your dashboard under API access.
curl -X POST https://api.webxtrend.com/v1.0/lookup \
-H 'Accept-Encoding: gzip' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"token": "<your_token>",
"query": "example.com"
}'
Each account has a daily query budget. It resets at midnight, and a 429 carries a Retry-After header with the exact seconds until then; read that rather than working out the boundary from your own clock.
| Plan | Queries per day |
|---|---|
| Free | 100 |
| Basic | 25,000 |
| Premium | 250,000 |
The API answers with 429 and a Retry-After header holding the seconds until reset.
{
"rcode": 429,
"error": "rate_limited",
"msg": "daily query limit reached",
"limit": 25000,
"used": 25000
}
HTTP/1.1 403
{
"rcode": 403,
"error": "after_subscription",
"msg": "Your subscription ended. Daily
exports up to 2026-10-03 stay available."
}error, not on msg. The message is English prose meant for a person and gets rewritten when a clearer wording turns up; several messages carry a date or a product name inside the sentence, so even an exact comparison would not hold. error stays as it is.The status code alone is not enough either, since seven different situations answer with 403. Only error tells them apart.
| Status | error | Meaning |
|---|---|---|
| 400 | query_missing | No query in the body, or it is empty. |
| 400 | domain_format | The value has no dot in it and cannot be read as a domain. An unknown suffix is not checked here; that comes back as 404 no_data. |
| 400 | date_missing | No date in the body. |
| 400 | date_format | The date is not YYYY-MM-DD. |
| 400 | type_invalid | type is not one of the seven record types. |
| 400 | followup_invalid | followup is not 7 or 30. |
| 400 | cursor_invalid | The next cursor cannot be read, or it was altered on the way. Pass back what you received rather than building one. |
| 400 | cursor_expired | The cursor is older than 24 hours. Start the run again from the first page. |
| 400 | cursor_mismatch | The cursor was issued for a different query. |
| 400 | cursor_needs_type | next without type on /dnshistory. |
| 401 | token_missing | No token in the body. |
| 401 | token_invalid | The token is unknown or no longer active. Generate a new one. |
| 403 | plan_required | The endpoint needs a Basic or Premium plan. |
| 403 | premium_required | The endpoint or option needs Premium. |
| 403 | addon_required | The export add-on has not been purchased. msg names which one. |
| 403 | no_subscription | The account has no subscription period at all. |
| 403 | before_subscription | The date lies before your first subscription. msg names the earliest one available. |
| 403 | after_subscription | Your subscription ended and the date lies after it. msg names your last available day. |
| 403 | outside_subscription | The date falls in a gap between two subscription periods. |
| 404 | no_data | Nothing stored for that domain, that date, or no snapshot built yet. Not an error in your request. |
| 404 | endpoint_unknown | No such endpoint under /v1.0/. |
| 429 | rate_limited | Daily limit reached. The response carries limit and used, plus a Retry-After header counting down to midnight. |
| 500 | internal_error | Something broke on our side. Worth retrying. |
/lookup, /redirect and /reverseip still wrap their error inside result rather than putting it at the top level, the way the other endpoints do. rcode, error and msg carry the same values in both shapes. Look one level deeper on those three.The key characteristics of a single domain in one call: whether a website answers, how it redirects, what its certificate looks like, and how many other domains point at it or share its address.
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Your API token. |
| query | string | required | The domain to look up, without scheme and without a trailing slash: example.com, not https://example.com/. Case is ignored. |
curl -X POST https://api.webxtrend.com/v1.0/lookup \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN", "query": "example.com" }'
{
"meta": {
"query": "example.com",
"rcode": 200,
"msg": "ok"
},
"result": {
"has_website": "yes",
"redirect_to_https": "no",
"redirect_to_www": "no",
"redirect_to_domain": "no",
"only_with_www": "no",
"only_dns_no_web": "no",
"ssl": "nocert",
"ip": "both",
"redirect_count": "1114",
"reverse_ip_count": "3",
"social_networks": [],
"social_accounts": []
}
}| Field | Type | Description |
|---|---|---|
| has_website | yes / no | A website is hosted under the domain and does not redirect somewhere else. |
| redirect_to_https | yes / no | The domain redirects from http to https. |
| redirect_to_www | yes / no | The domain redirects to www. plus itself. |
| redirect_to_domain | yes / no | The domain redirects to a different domain. Which one, and who else points there, is what /redirect answers. |
| only_with_www | yes / no | Reachable only as www.example.com, the bare domain answers nothing. |
| only_dns_no_web | yes / no | The domain has DNS entries but no A or AAAA record, so nothing can be reached over the web. |
| ssl | valid / nocert / invalid | nocert means no certificate was presented, invalid that one was presented but did not validate: expired, self-signed or issued for another name. |
| ip | v4only / v6only / both / servfail / noip / nxdomain | Which address families resolve. nxdomain means the domain is not in the zone right now. It is not a statement about the past: a domain that carries this flag was usually registered once and has been deleted or has expired, and the records you get for it describe that earlier state, not today. servfail means the responsible name server failed to answer. The field is left out entirely for domains whose resolution has not been recorded yet. It is the one key in result that can be missing. |
| redirect_count | number | How many domains redirect to this one. Returned as a string. |
| reverse_ip_count | number | How many domains currently share its IP addresses, summed over every address the domain resolves to, which is what /reverseip returns. A domain on two of those addresses counts once per address. Returned as a string. |
| social_networks | array of strings | The networks the domain is present on, for example facebook, instagram, x, linkedin, youtube, tiktok, pinterest. Empty array when none were found. Use this to filter without walking social_accounts. |
| social_accounts | array of objects | The accounts themselves, each with network, type, username and url. A domain can have more than one account per network. type is finer than network: a Facebook entry can be facebook or facebook-pages, a LinkedIn one linked-in-company. Treat it as a label, not as a closed set: it comes from the crawler and new values appear when new link patterns do. The links are the ones found on the crawled start page, so they are what the site itself points to, not what the network says about the domain. Empty array when none were found, which is the case for example.com above. A domain that has them looks like this:"social_networks": ["instagram", "facebook", "linkedin"],
"social_accounts": [
{
"network": "facebook",
"type": "facebook",
"username": "heiseonline",
"url": "https://facebook.com/heiseonline"
},
{
"network": "linkedin",
"type": "linked-in-company",
"username": "heiseonline",
"url": "https://de.linkedin.com/company/heiseonline"
}
] |
| Status | error | When |
|---|---|---|
| 400 | query_missing | No query in the body. |
| 400 | domain_format | The value has no dot in it, so there is nothing to read as a domain. A name with an unknown suffix passes this check and comes back as 404 no_data. |
The errors every endpoint can return (invalid token, daily limit) are listed under Errors & limits.
Every domain that redirects to the one you ask about. Useful for finding the parked or expired names a brand collects, and for spotting typo domains pointed at a site.
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Your API token. |
| query | string | required | The target domain, the one others redirect to. |
| limit | number | optional | Rows per page. Default 1,000. Free stays at 1,000, Basic caps at 10,000, Premium at 50,000; a higher value is silently reduced to the cap rather than refused. |
| next | string | optional | The cursor from the previous response, passed back verbatim. It is tied to the query it was issued for; sending it with a different domain is refused. |
curl -X POST https://api.webxtrend.com/v1.0/redirect \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN", "query": "example.com" }'
{
"meta": {
"query": "example.com",
"limit": 1000,
"max_limit": 10000,
"rcode": 200,
"msg": "ok",
"next": null
},
"result": [
{"target": "coovii.co.jp"},
{"target": "supara.fun"},
{"target": "aethra.org"}
]
}| Field | Type | Description |
|---|---|---|
| result[].target | string | A domain that redirects to the one you asked about. Despite the name, this is the source of the redirect. |
| meta.limit | number | The page size actually applied: your value, or your plan’s ceiling where it was higher. |
| meta.max_limit | number | The ceiling your plan allows for limit. |
| meta.next | string / null | Cursor for the following page. null means you have seen everything. That, not an empty result, is the signal to stop. |
next set to the value you just received, leaving query unchanged. Keep going until meta.next comes back null. Every call counts against your daily quota and a cursor expires after 24 hours, so on a target with millions of rows a larger limit is not just faster; it decides whether you get through at all.| Status | error | When |
|---|---|---|
| 400 | query_missing | No query in the body. |
| 400 | domain_format | The value does not parse as a domain. |
| 400 | cursor_invalid | The next cursor could not be read, or it was altered on the way. Do not build one yourself; pass back what you received. |
| 400 | cursor_expired | The cursor is older than 24 hours. A run is meant to be walked through in one go. Start again from the first page. |
| 400 | cursor_mismatch | The cursor was issued for a different query. |
The neighbours: every domain that currently resolves to the same address as the one you ask about. On shared hosting that is the other sites on the box; on a dedicated address it is usually just the domain itself.
A domain rarely has just one address: an A record and an AAAA record, or several behind a round robin. The answer covers all of them, and every row names the address it belongs to. You can also ask with an address directly and skip the domain.
Only the present state is returned. A domain that moves, or stops resolving altogether, drops out of the answer on its next crawl, and addresses it used to sit on are not included.
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Your API token. |
| query | string | required | A domain, or an IP address. With a domain you get the neighbours on every address it resolves to; with an address, the domains on that one. IPv4 and IPv6 both work, and an IPv6 may be written either way: 2606:2800:0220:0001:0000:0000:0000:0001 and 2606:2800:220:1::1 are the same query. Addresses are stored numerically, so the two forms are the same value, not two spellings that have to be tried. |
| limit | number | optional | Rows per page. Default 1,000. Free stays at 1,000, Basic caps at 10,000, Premium at 50,000; a higher value is silently reduced to the cap rather than refused. |
| next | string | optional | The cursor from the previous response, passed back verbatim. It is tied to the query it was issued for. Paging walks the addresses one after another, so a page may end in the middle of one and the next page carries on there. |
curl -X POST https://api.webxtrend.com/v1.0/reverseip \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN", "query": "example.com" }'
{
"meta": {
"query": "example.com",
"limit": 1000,
"max_limit": 10000,
"rcode": 200,
"msg": "ok",
"ips": ["104.20.23.154", "2606:4700:10::6814:179a"],
"ip_count": 2,
"next": null
},
"result": [
{"target": "altair-labs.de", "ip": "104.20.23.154", "since": "2026-04-14T02:23:44"},
{"target": "kranich-versand.at", "ip": "104.20.23.154", "since": "2026-08-02T05:43:46"},
{"target": "nordlicht.shop", "ip": "2606:4700:10::6814:179a", "since": "2026-09-16T08:25:46"}
]
}| Field | Type | Description |
|---|---|---|
| result[].target | string | A domain sharing an IP address with the one you asked about. The field carries the same name as in /redirect, but means something else here: a neighbour, not a redirect source. |
| result[].ip | string | The address this row belongs to, always in its shortest form (2606:4700:10::6814:179a, never the written-out variant). With several addresses in play, this is what tells them apart; group on it to get one neighbourhood per address. |
| result[].since | string | When this domain first appeared on this address, in the same notation as first_seen in /dnshistory. That the row is here at all already means it is there now, and this tells you for how long. It replaces the short-lived last_seen, which answered a question the present-state index no longer asks. |
| meta.ips | array | Every address the domain currently resolves to, in the order the rows come in. Asked with an address, it holds just that one. |
| meta.ip_count | number | How many addresses that is. |
| meta.limit | number | The page size actually applied: your value, or your plan’s ceiling where it was higher. |
| meta.max_limit | number | The ceiling your plan allows for limit. |
| meta.next | string / null | Cursor for the following page, null when the list is exhausted. |
meta.ips holds the slice we last resolved, not the whole pool.| Status | error | When |
|---|---|---|
| 400 | query_missing | No query in the body. |
| 400 | domain_format | The value parses as neither a domain nor an IP address. |
| 400 | cursor_invalid | The next cursor could not be read, or it was altered on the way. |
| 400 | cursor_expired | The cursor is older than 24 hours. A run is meant to be walked through in one go. Start again from the first page. |
| 400 | cursor_mismatch | The cursor belongs to another query. |
| 404 | no_data | Nothing on record for that domain or address. |
How a domain’s records changed over time. Each entry is a state with the date it was first seen, so you can follow a move between hosters, a change of mail provider or the moment a domain was parked.
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Your API token. |
| query | string | required | The domain to trace. |
| type | A, AAAA, MX, TXT, NS, SOA, CNAME | optional | One record type. Left out, you get all seven at once, which is convenient for a first look but cannot be paged. |
| limit | number | optional | Entries per record type. Default 10. Basic caps at 10, Premium at 100; a higher value is silently reduced to the cap rather than refused. |
| next | string | optional | The meta.next of the previous response, passed back verbatim. It names the domain and the record type itself, so type may be left out alongside it; sent anyway, it has to agree. A bare first_seen from the response also works and then does need type. |
curl -X POST https://api.webxtrend.com/v1.0/dnshistory \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN", "query": "example.com", "type": "NS" }'
{
"meta": {
"query": "example.com",
"rcode": 200,
"count": 2,
"max_limit": 100,
"totals": {"NS": 2}
},
"result": {
"NS": [
{
"first_seen": "2025-12-17T22:30:00",
"records": [
{"nameserver": "elliott.ns.cloudflare.com"}
]
}
]
}
}| Field | Type | Description |
|---|---|---|
| meta.count | number | How many states this response contains. |
| meta.max_limit | number | The ceiling your plan allows for limit: 10 on Basic, 100 on Premium. |
| meta.limit | number | The limit actually applied: your value, or your plan’s ceiling where it was higher. |
| meta.totals | object | How many states exist per record type, regardless of limit. Use it to tell “that is all there is” apart from “the page was full”. |
| meta.totals_capped | array | Record types whose count stopped at 1,000. Their number in totals reads as “at least”. Absent when nothing was capped. |
| meta.truncated | array | Record types that have older states beyond this page. Absent when every type is complete. |
| meta.next | string | The cursor for the following page, present only when you asked for a single type and that type is truncated. On a request without type the field is absent; truncated then names the types to continue one at a time. |
| result.<type>[].first_seen | string | When this state was first observed, as YYYY-MM-DDThh:mm:ss. It can be passed back as next together with type, useful for resuming days later from a value you stored. For paging through in one go, meta.next is the shorter path. |
| result.<type>[].records | array | The records of that state. Their shape depends on the type (see below). |
type, then repeat the call with next set to the meta.next you just received, until that field is gone. Without type there is no cursor: a collection call is a first look, and meta.truncated tells you which types to go through afterwards.| Type | Fields of each entry in records |
|---|---|
| A | ipv4_address |
| AAAA | ipv6_address |
| NS | nameserver, the authoritative name server. |
| CNAME | target, the name the alias points to. |
| MX | priority, mail_server; lower priority wins. |
| TXT | text, parts; DNS splits anything longer than 255 bytes, so a long SPF or DKIM record arrives in several character strings. parts keeps them, text is those parts joined. |
| SOA | primary_nameserver, admin_email, serial, refresh_seconds, retry_seconds, expire_seconds. The zone stores the administrator’s address as a domain name, where the first dot stands for the @; admin_email is that address written out. minimum_ttl_seconds is present only where the zone reported it; older history predates its collection, and the field is then left out rather than returned as null. |
| Status | error | When |
|---|---|---|
| 403 | plan_required | The DNS history needs a Basic or Premium plan. |
| 400 | query_missing | No query in the body, or it is empty. |
| 400 | domain_format | The value does not parse as a domain. |
| 400 | type_invalid | Not one of A, AAAA, MX, TXT, NS, SOA, CNAME. |
| 400 | cursor_needs_type | You sent next without type; paging works one record type at a time. |
| 400 | cursor_invalid | next is neither a cursor from a previous response nor a first_seen value. |
| 400 | cursor_expired | The cursor is older than 24 hours. A run is meant to be walked through in one go. Start again from the first page. |
| 400 | cursor_mismatch | The cursor was issued for a different domain, or for another record type than the type you sent alongside it. |
Every domain registered on one day, as a gzipped list with one name per line. The file for a day is written the following night, so the newest one available always carries yesterday’s date.
.txt.gz file itself, not JSON. Only an error comes back as JSON, so check the status code before you unpack.wget or a cron job as a plain URL: ?token=…&date=2026-09-03&followup=7. That is the form the explorer shows under “Request URL”. Keep in mind that the token then sits in the URL, where proxies and shell history can see it.| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Your API token. |
| date | YYYY-MM-DD | required | The registration day. Anything but this exact shape is refused before the file is looked up. |
| followup | 7 or 30 | optional | Instead of the bare name list, the follow-up file for that day: what became of those domains a week or a month later. Premium only. |
curl -X POST https://api.webxtrend.com/v1.0/daily \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN", "date": "2026-09-03" }' \ -o 2026-09-03.txt.gz
HTTP/1.1 200 Content-Type: application/gzip Content-Disposition: attachment; filename="2026-09-03.txt.gz" Content-Length: 1272043 # unpacked, one domain per line: nordlicht.shop altair-labs.de kranich-versand.at
| followup | Contains | Typical size |
|---|---|---|
| 7 | State one week after registration | ~324,000 domains |
| 30 | State one month after registration | ~389,000 domains |
These carry website, mail, SSL and social columns alongside the name, which is what makes a freshly registered domain worth judging: on registration day almost none of them resolve to anything yet.
2026-10-03 as its final delivery. Days you paid for stay available after the subscription ends; days outside any period are refused with 403.| Status | error | When |
|---|---|---|
| 403 | plan_required | Daily downloads need a Basic or Premium plan, and the account has neither now nor in the past. |
| 400 | date_missing | No date in the body. |
| 400 | date_format | Not YYYY-MM-DD. |
| 403 | before_subscription | The date lies before your first subscription. The message names the earliest one available to you. |
| 403 | after_subscription | Your subscription has ended and the date lies after it. The message names your last available day. |
| 403 | outside_subscription | The date falls in a gap between two subscription periods. |
| 403 | no_subscription | The account has no subscription period at all. |
| 403 | premium_required | followup on a period that ran on Basic. |
| 400 | followup_invalid | Not 7 or 30. |
| 404 | no_data | The day is within your period, but no file was produced. |
One file with every registered domain we know of, over a billion names. Without download the call reports what is available; with it you get the file.
/daily, the call also works as a GET with the parameters in the query string (?token=…&download=1), which is the easier form for a file of this size. The token is then part of the URL.| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | required | Your API token. |
| download | "1" | optional | Send the file instead of the report. Default: report only. |
| enriched | "1" | optional | The enriched edition, with website, mail and SSL columns beside each name. Needs its own add-on. |
curl -X POST https://api.webxtrend.com/v1.0/snapshot \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN" }'
curl -X POST https://api.webxtrend.com/v1.0/snapshot \ -H 'Content-Type: application/json' \ -d '{ "token": "YOUR_TOKEN", "download": "1" }' \ -o domains.txt.gz
{
"meta": {
"rcode": 200,
"msg": "snapshot available",
"generated": "2026-09-01T03:12:44+02:00",
"domains": 1148392011,
"size": 1073741824,
"filename": "domains-2026-09-01.txt.gz"
},
"result": []
}| Field | Type | Description |
|---|---|---|
| meta.generated | string | When the snapshot was built, with time zone offset. A new one is produced nightly. |
| meta.domains | number | How many names the file contains. |
| meta.size | number | Compressed size in bytes. Worth checking before you start the transfer: this is a multi-gigabyte file. |
| meta.filename | string | The name the download will carry. |
| meta.format | names / enriched | Which edition the figures above describe, so a client that passes enriched can confirm it got what it asked for. |
| meta.enriched_available | true / false | Whether this account may request the enriched edition. It is answered on every call, including one without enriched, so you can tell an add-on you already hold from one you would still have to buy without provoking a 403. |
| Status | error | When |
|---|---|---|
| 403 | addon_required | The export has not been purchased, also for enriched without that second add-on. The message names which one is missing. |
| 404 | no_data | No snapshot has been built yet. |
If you need additional endpoints, bulk exports, or higher limits, reach out to us.