Conventions shared by every tool
The rules here apply across the DataPulse tools: how names and URLs are put into canonical form, the hashes that key stored results, how list answers page, what an answer that finds nothing looks like, how a bulk receipt accounts for every input, what is cached, and which access each tool needs. Tool topics refer back to these sections instead of restating them.
Names and URLs
What every name goes through
Every domain / query parameter is put into one canonical form before anything is
sent:
| Step | Example |
|---|---|
| Surrounding whitespace removed | " example.com " → example.com |
| ASCII case folded | Example.COM → example.com |
Unicode label separators folded to . (。 . 。) | example。com → example.com |
| Fullwidth and other compatibility characters mapped (UTS-46) | example.com → example.com |
| IDN converted to its A-label (punycode) form | bücher.de → xn--bcher-kva.de |
| Every trailing dot removed, with any whitespace among them | example.com., example.com.., example.com. . → example.com |
The U-label and A-label spellings of a name are the same lookup. A run of trailing
dots is input noise, not an empty label: example.com.. is example.com. An empty
label anywhere else (a..b.com, .example.com) is refused, and so is whitespace
inside a name (exa mple.com).
Refused wherever a domain is required: a URL (https://example.com/path), a port
(example.com:8080), credentials, an IP literal (except where a tool takes
addresses), a label over 63 bytes or a name over 253, a label starting or ending with
a hyphen. The refusal names the reason and nothing is sent.
Names that registries sold but IDNA 2008 forbids (emoji and symbol names, raw or as
xn-- punycode) pass local validation in every tool except datapulse_disputedb_lookup,
which is strict. The Scrape API then refuses them for scrapes, the domain overview, live
DNS and domain health (invalid_url or invalid_domain; invalid_input on a bulk item,
as the DNS bulk example in datapulse_help(topic="dns") shows for xn--ls8h.la). Live
RDAP accepts them.
Three models: how much of the name is kept
| Model | Tools | What is looked up |
|---|---|---|
| Registration: reduced to the registrable domain | datapulse_live_rdap, datapulse_live_rdap_bulk, datapulse_dns_rdap_history | Hostname labels above the longest public suffix are removed (full Public Suffix List, private suffixes included): WWW.Example.COM. → example.com, www.microsoft.jp.net → microsoft.jp.net. The two live RDAP tools also take IPv4/IPv6 addresses, in canonical form. |
| Hostname kept: canonical spelling, no labels removed | datapulse_live_dns, datapulse_live_dns_bulk, datapulse_dns_dphistory, datapulse_dns_dptechsim, datapulse_dns_neighborhood, datapulse_live_domain_health, datapulse_domain_overview, datapulse_disputedb_lookup (domain) | Exactly the name given, canonicalized. www.example.com and example.com are different names and are looked up separately. |
| URL | datapulse_scrape_submit, datapulse_scrape_bulk | Scheme and host are case-folded and the host canonicalized as above; the port is kept as given, and path, query and fragment are sent byte for byte. Only http: and https: are accepted; credentials, scheme-relative URLs, IP-address hosts and other schemes are refused. A URL without a scheme is tried over https, then http. |
Two hostname-kept tools add handling downstream without changing what is sent: the
domain-health probe checks both the name and its www. sibling, and DisputeDB
matches a leading www. away itself, so www.tesla.com and tesla.com find the
same decisions (the response’s query shows the name as sent).
Single labels and underscores
| Input | Accepted by | Refused by |
|---|---|---|
A single label (com, example) | datapulse_live_dns, datapulse_live_dns_bulk: a TLD or a bare host is a fair DNS question. localhost and names under it are refused (RFC 6761: the resolver answers them itself) | Every other tool, before any request: nothing registrable, hosted, scraped or disputed has fewer than two labels |
An underscore in a hostname label (_dmarc.example.com) | Every tool. The registration tools reduce it to example.com | — |
An underscore in the registrable domain (exa_mple.com) | The hostname-kept tools except domain health | datapulse_live_rdap, datapulse_live_rdap_bulk, datapulse_dns_rdap_history, datapulse_live_domain_health: no registry registers such a name |
submitted_domain / submitted_url
When the name looked up differs from the input by more than ASCII case (an IDN folded
to punycode, a trailing dot dropped, a hostname reduced to its registrable domain),
the response carries submitted_domain (submitted_url for datapulse_scrape_submit)
with the original input. It is absent when nothing changed, and it keeps a homograph
such as instagrạm.com from reading as a report about instagram.com.
| Tool | Echo |
|---|---|
datapulse_live_rdap, datapulse_dns_rdap_history, datapulse_dns_dphistory, datapulse_live_dns, datapulse_live_domain_health, datapulse_domain_overview, datapulse_disputedb_lookup | submitted_domain field |
datapulse_scrape_submit | submitted_url field |
datapulse_dns_dptechsim, datapulse_dns_neighborhood | A hint line after the CSV or rows: submitted_domain: <input> (sent as <name>) |
| Bulk tools | No echo; each index maps the canonical item back to the input array |
Hashes
Stored results are keyed by SHA-256 hashes of canonical forms. Each is returned by the tool that creates the row, and reading a row back by its hash makes no new lookup.
| Hash | Hashes | Returned by | Reads the row back |
|---|---|---|---|
domain_hash | The canonical domain with a leading www. removed | Scrape, overview, DNS, RDAP and domain-health tools | datapulse_live_rdap(domain_hash=...) reads a stored RDAP row; it is also the join key between datapulse_domain_overview, datapulse_live_domain_health and scrape rows |
url_hash | The URL’s identity form (below) | datapulse_scrape_submit, datapulse_scrape_bulk (sent[].url_hash), every scrape row | datapulse_scrape_result(url_hash=...), datapulse_scrape_bulk_results(url_hashes=[...]) |
dns_query_hash | The canonical name as sent (www. kept), with the record type folded in when one was asked for | datapulse_live_dns, datapulse_live_dns_bulk (submitted[].dns_query_hash) | datapulse_live_dns(dns_query_hash=...) |
A root URL’s url_hash equals its domain’s domain_hash: both hash example.com.
The hashes are returned as data; computing them outside the server is not needed.
URL identity
The identity form behind url_hash normalizes more than the request does. These are
the same stored page: http:// and https://; with and without a leading www.;
host case and U-label / A-label spellings; with and without the default port (:80,
:443); with and without a #fragment; a bare trailing / on the root; query
parameters in any order; ./.. path segments and their resolved form; percent-
encoded and literal spellings of the same character. Path case and a trailing /
on a non-root path are significant, and a non-default port is part of the identity.
Paging
List answers are bounded. Each list tool’s description states its default and largest page and the size of a row.
| Field | Meaning |
|---|---|
total | Rows in the whole set (absent where the source cannot count them) |
returned | Rows on this page |
offset | Rows skipped before this page |
truncated | true when more rows exist beyond this page |
next_offset | The offset of the next page, present when truncated is true |
note | Names the next call (pass offset=N); a truncated page is a SUBSET of the set |
counts | Per-category breakdowns beside total, where a tool has them |
limitabsent,0or negative means the default; above the maximum it is capped, not refused. A negativeoffsetis refused. The sameoffsetreturns the same rows.complete: falseis a different signal: the source stopped early (a back-end result cap or compute limit), so paging cannot reach the rest. The remedy is a narrower query, as the hint says. CSV tools put it as a hint line, never inside the rows.- A nested list that is capped inside a row carries
<field>_totaland<field>_truncatedbeside it. - Long text comes in parts:
part=N, ortext_offset/next_text_offset, each part naming the call for the next.
Arguments
- An integer or boolean sent as a string (
"20","true","0.9") is read as the value it spells. A fraction where an integer is required, a value outside the 32-bit range, NaN and infinity are refused with the value sent. - JSON
nullis an absent argument. When a tool chooses a mode from “exactly one of” selectors, a blank string (empty or whitespace) selects nothing either. - An argument that belongs to the mode not chosen is refused when it selects
something; one holding its documented default (
offset: 0,force: false) selects nothing and is accepted. - An unknown argument is refused with the list of accepted ones. String arguments have a maximum length, counted in characters; a value echoed in an error is cut to 64 characters.
Answers that find nothing
“No data for this input” is an answer, not an error:
- Lookups by name or hash (
datapulse_dns_dphistory,datapulse_dns_rdap_history, stored-row reads bydomain_hash,dns_query_hash,url_hash, anddatapulse_scrape_list(batch_id=...)for an unknown batch) answerfound: falsewith the identifying field, the back end’s own reason asdetailwhen it gave one, and anotenaming the next call. datapulse_disputedb_lookupwith a case number that does not exist answersfound: falsewithtotal: 0.- CSV tools (
datapulse_dns_dptechsim,datapulse_dns_searchlabels) answer with the header row and a hint line. datapulse_scrape_bulk_resultslists hashes with no row innot_found.- An empty list is
[], nevernull.
Bulk receipts
datapulse_live_dns_bulk, datapulse_live_rdap_bulk and datapulse_scrape_bulk
validate every item before the request; one bad item never costs the batch (a batch
with nothing usable is an error).
| Field | Meaning |
|---|---|
accepted, cached, rejected | The API’s own counts over the items that were sent: queued, answered from cache, refused by the API |
rejected_items (rejected_urls for scrape) | What the API refused: index, query (url) and the API’s error code. rejected counts these |
rejected_local | What was refused before the request (malformed, blank, a single label or reserved address where not allowed): index, query (url) and the reason. Not counted in rejected |
duplicates | Items that normalize to an earlier item and are not sent: index, the item as written, first_index. DNS and RDAP compare the canonical name (and record type); scrape compares url_hash |
submitted (sent for scrape) | Every item that was sent, in canonical form, with its hashes and the API’s job_id / cached when it reported them. Sent is not accepted: an item the API refused appears here and in rejected_items |
Every index is the position in the array passed. Every input is accounted for:
accepted + cached + rejected + len(rejected_local) + len(duplicates) = number of items passed
Finding every failure takes both rejected_items (rejected_urls) and
rejected_local. Each tool’s topic names how its results are read back.
Caching
| Family | Cache |
|---|---|
| Web scrapes | A URL scraped successfully in the last 90 days, or whose scrape failed in the last 7 days, is served from storage (cached: true); force=true scrapes again |
| Live DNS | Not cached by the application; only the resolver caches. cached is always 0 on a DNS bulk receipt |
| Live RDAP (domains) | Served from the Scrape API’s cache or the DataPulse database when one holds the name, otherwise looked up live; source says which and force=true looks up live. The bulk tool uses the Scrape API’s 90-day cache |
| Live RDAP (IPs) | A recent RIR answer from cache, otherwise the RIR; force=true refreshes |
| Domain health | Every call runs a new probe; the latest report for a domain is kept about 24 hours |
| History, similarity, clusters, search, UDRP, AGB, applications | Served from their corpora as of their last load |
Access and scopes
Sign-in is OAuth 2.1 with PKCE (S256). The server’s
/.well-known/oauth-protected-resource document names the authorization server.
Clients identify themselves with a Client ID Metadata Document; the server offers no
dynamic client registration. The access token goes in the Authorization: Bearer
header only (query-string and form tokens are refused), and the server reads the
caller’s permissions from it.
Every request needs the datapulse:mcp permission; without it the answer is 403
insufficient_scope. Each tool also needs the scope below, if any:
| Answer | Meaning |
|---|---|
| 401 | No token, or one that failed validation (expired, wrong audience or issuer, bad signature). WWW-Authenticate names the metadata document |
403 insufficient_scope | A valid token without datapulse:mcp or without the tool’s scope |
403 Origin not allowed | A browser request from an origin the server does not accept |
| 429 | Too many requests for this account in a short time |
A tool call runs for at most 120 seconds. Each account may have only a few slow calls
(scrapes with wait=true, domain overviews, domain-health checks, bulk submissions)
running at once; one more is
refused as a tool error saying how many are running.
Generated from the live server (DataPulse MCP v0.9-323-g8d4469c) on October 19, 2026. Your AI assistant reads this page by calling datapulse_help(topic="conventions").