The tools behind toyroom.ai behind one key and one quota. Appraise names with the in-house model trained on real aftermarket sales. Classify them by industry. Count keywords across every registered domain. Look up registration and DNS facts. Two surfaces: REST and MCP.
Keys are secrets. Keep them server-side, never in browser code or committed files. This origin sends no CORS headers, so a key cannot be used from a web page. If a key leaks, ask for a replacement: revocation is immediate.
Quickstart
Appraise a domain in one call. Pick your language once; the choice sticks for every example on this page.
Each key has a daily credit quota that resets at 00:00 UTC and is shared by REST and MCP. Each endpoint also has a per-minute request limit. Nothing is charged for a 4xx or a 502.
Endpoint
Credits
Rate limit
Batch max
/v1/appraise
1 credit
30/min
/v1/appraise/bulk
1 credit per domain
6/min
500 domains
/v1/categorize
2 credits per domain
12/min
100 domains
/v1/camelcase
1 credit
60/min
1,000 domains
/v1/zone/count
2 credits
20/min
50 extensions
/v1/zone/lookup
1 credit per 10 names
12/min
5,000 domains
/v1/zone/stats
1 credit
20/min
/v1/rdap
2 credits per domain
6/min
10 domains
/v1/dns
1 credit per domain
12/min
50 domains
/v1/jobs
reserved at submit
12/min
/v1/jobs/{job_id}
free
none
/v1/jobs/{job_id}/results
free
none
/v1/jobs
free
none
/v1/jobs/{job_id}
free
none
/v1/usage
free
none
/v1/ping
free
none
See today's balance and every limit with GET /v1/usage. It is free and never rate limited.
RDAP is deliberately tight. Registries enforce global rate caps that every client on the internet shares. Registration data changes slowly: cache it.
Working at scale
Every endpoint below answers inside the request. For a list that fits in a few requests, batch it at the endpoint's batch max, send at the endpoint's rate, and honour Retry-After on any 429. For anything larger, or anything you do not want to babysit, submit a job and let Toyroom hold the loop. These are the ceilings a single key can reach with the synchronous endpoints:
Endpoint
Per request
Per minute
Per hour
Credits
/v1/appraise/bulk
500
3,000
180,000
1 credit per domain
/v1/categorize
100
1,200
72,000
2 credits per domain
/v1/camelcase
1,000
60,000
3.6M
1 credit per request
/v1/zone/lookup
5,000
60,000
3.6M
1 credit per 10 names
/v1/zone/count
1 term
20
1,200
2 credits per term
/v1/dns
50
600
36,000
1 credit per domain
/v1/rdap
10
60
3,600
2 credits per domain
Three rules make a long run boring in the good way:
Pace, do not poll. A 429 costs a round trip and nothing else, but sleeping between batches is cheaper than bouncing off the limit.
Retry 502 with backoff. A 502 means the tool behind the endpoint did not answer and no credits were charged. Retry the same batch.
Size your quota first. Multiply your list by the credits per domain and compare with daily_quota from /v1/usage. Ask for a larger quota before you start, not after the 429.
python · appraise a file of names
import os, time, requests
KEY = os.environ["TOYROOM_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
domains = [d.strip() for d in open("names.txt") if d.strip()]
results = []
for i in range(0, len(domains), 500): # batch max for /v1/appraise/bulk
chunk = domains[i:i + 500]
while True:
r = requests.post("https://api.toyroom.ai/v1/appraise/bulk", headers=H, json={"domains": chunk})
if r.status_code == 429: # rate or quota: the header says how long
time.sleep(int(r.headers.get("Retry-After", "30")))
continue
if r.status_code == 502: # nothing charged: retry with backoff
time.sleep(5)
continue
r.raise_for_status()
results.extend(r.json()["results"])
break
if (i // 500) % 6 == 5: # 6 requests/min: pace instead of bouncing off 429
time.sleep(60)
print(len(results), "appraised")
Run a million names through the Zone Counter
Submit the file once as a job. Toyroom splits it into chunks, runs them, retries the ones that hiccup, and writes one results file in your input order. You get a callback when it is done, or poll, then download. Use POST /v1/jobs with type: zone_lookup, not /v1/zone/count, which is one keyword per request and exists for keyword research.
Size the quota. Zone lookup costs 1 credit per 10 names, so a million names is 100,000 credits, or 200,000 with include_counts. The job is refused up front if today's quota cannot cover it. Check daily_quota on /v1/usage; if it is smaller, write to [email protected] with the list size and we will raise it.
Submit. Send the names as JSON, or as a text file one name per line (gzip if you like). The 202 tells you the queue position and an estimated finish, about 17 minutes for a million names from an idle lane.
Wait for the callback, or poll. A signed POST arrives at your callback_url when the job finishes. Without one, poll the job every 30 seconds.
Download. One file, NDJSON or CSV, rows in input order. It stays for 24 hours.
python · submit a file as a job, wait, download zone.csv
import os, time, requests
KEY = os.environ["TOYROOM_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
API = "https://api.toyroom.ai"
# 1. submit the file as-is: one name per line
with open("names.txt", "rb") as f:
r = requests.post(f"{API}/v1/jobs", headers={**H, "Content-Type": "text/plain"},
params={"type": "zone_lookup"}, data=f, timeout=120)
r.raise_for_status()
job = r.json()
print(job["id"], "queued at position", job.get("position"), "eta", job["estimated_finish"])
# 2. poll (or skip this and wait for the callback_url POST instead)
while job["status"] in ("queued", "running"):
time.sleep(int(r.headers.get("Retry-After", "30")))
r = requests.get(f"{API}{job['poll_url']}", headers=H, timeout=30)
job = r.json()
print(f"{job['status']} {job['done']:,}/{job['total']:,}")
# 3. download one file
if job["results_url"]:
with requests.get(f"{API}{job['results_url']}", headers=H, params={"format": "csv"}, stream=True, timeout=600) as d:
d.raise_for_status()
with open("zone.csv", "wb") as out:
for chunk in d.iter_content(1 << 16):
out.write(chunk)
print("saved zone.csv; credits charged:", job["credits_charged"])
Prefer to hold the loop yourself?POST /v1/zone/lookup takes 5,000 names per request at 12 requests per minute: 200 requests and about 17 minutes for a million names. The script below paces itself and writes as it goes.
python · the same million names through the synchronous endpoint
import csv, os, time, requests
KEY = os.environ["TOYROOM_KEY"]
H = {"Authorization": f"Bearer {KEY}"}
BATCH = 5000 # batch max for /v1/zone/lookup
names = [d.strip() for d in open("names.txt") if d.strip()] # one domain per line
with open("zone.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["input", "name", "extension", "registered", "extensions_registered"])
for i in range(0, len(names), BATCH):
chunk = names[i:i + BATCH]
while True:
r = requests.post("https://api.toyroom.ai/v1/zone/lookup", headers=H,
json={"domains": chunk}, timeout=120)
if r.status_code == 429: # rate or quota: wait what the header says
time.sleep(int(r.headers.get("Retry-After", "30")))
continue
if r.status_code == 502: # index unavailable, nothing charged
time.sleep(5)
continue
r.raise_for_status()
break
for row in r.json()["results"]: # same order as chunk
out.writerow([row["input"], row["name"], row["extension"],
row["registered"], row["extensions_registered"]])
time.sleep(5) # 12 requests/min
print("done:", len(names), "names")
What you get back is counts and booleans. Registered or not, how many extensions, how many domains contain the name. Never a list of domains. That keeps every row safe to store and publish.
How jobs behave
Type
Per name
Credits
Throughput
zone_lookup
Registered or not and extension counts per name; include_counts adds keyword counts
1 credit per 10 names (doubled with counts)
about 20,000 names per second
appraise
v2.1 value estimate per domain
1 credit per domain
about 1,800 domains per second
categorize
Industry vertical and subcategory per domain
2 credits per domain
about 5,000 domains per second
Limits. 1,000,000 names per job, 5,000,000 names queued or running per key across any number of jobs, 32 MB per submission.
Credits. Reserved when the job is accepted. Refunded for every row never processed: on cancel, on expiry, when a job stops at its runtime budget, and for rows the tool could not answer after three attempts.
Never stuck. A submission the lane cannot start within 2 hours is refused with the estimated wait and nothing charged. A job still queued at its deadline expires and refunds in full. A job that outruns its budget stops with partial results downloadable. A worker that goes silent has its job reclaimed and resumed from the last chunk.
Callbacks. HTTPS only. One POST per final state, with Toyroom-Event and a Toyroom-Signature header (t=timestamp,v1=HMAC-SHA256(secret, timestamp + "." + body)) using the webhook secret issued with your key. Six attempts over four hours on any non-2xx, then callback.status reads gave_up; the job and its file are unaffected.
Retention. Results are deleted 24 hours after the job finishes. Delete earlier with DELETE /v1/jobs/{id}.
POST/v1/appraise
1 credit
Appraise a domain
Returns a calibrated USD estimate with a low and high range, the word split and industry classification that fed the valuation, and how many extensions the same second-level name is registered under.
Deterministic classification from a curated keyword lexicon and domain-type rules. Every result carries the matched keywords, a confidence level, secondary tags, and a domain type (industry, brandable, geo, and so on). No language model is involved, so the same input always returns the same answer.
Rate 12 requests/minCost 2 credits per domainBatch 100 domains
Domain type: industry, brandable, geo, personal, and so on.
summary
object
Counts by vertical, subcategory, and type for the whole batch.
POST/v1/camelcase
1 credit
Split words
Dictionary-driven splitting with per-domain confidence, plus word, length, and extension statistics for the whole batch. One credit covers the entire request.
Searches the Toyroom zone index: 390M+ registered domains across 23,000+ extensions, rebuilt from daily zone-file snapshots. Returns totals and a per-extension breakdown. Counts only: this endpoint never returns domain names, and that is a product boundary, not a missing feature.
Rate 20 requests/minCost 2 creditsExtensions up to 50 per query
Request
Field
Type
Description
term
string
required
Keyword, 2 to 63 characters, letters, digits, and hyphens.
tlds
string[]
optional
Restrict the breakdown to these extensions, up to 50. Omit for all.
Domains whose second-level name matches, within the requested extensions.
exact_domain_count
integer
Domains whose second-level name is exactly the term.
keyword_count
integer
Distinct second-level names that match.
tld_match_count
integer
Matches where the term also appears in the extension.
tld_breakdown[]
object[]
Count per extension, largest first.
total_domains_in_db
integer
Size of the index the counts were taken from.
Counts only. Zone endpoints never return domain names. That is a product boundary set by ICANN zone-file access terms, not a missing feature. Every number is safe to publish.
POST/v1/zone/lookup
1 credit per 10 names
Look up a list of names
The bulk form of the Zone Counter. Send full domains or bare names. For each one you get whether that exact domain is in the index and how many extensions the name is registered under. Add include_counts to also get how many registered domains contain, start with, or end with the name (slower, and doubles the cost). Rows come back in input order. Counts and booleans only: no domain names are ever returned.
Rate 12 requests/minCost 1 credit per 10 namesBatch 5,000 domains
Request
Field
Type
Description
domains
string[]
required
Up to 5,000 domains or bare names. The first label is the name, the rest is the extension.
include_counts
boolean
optional
Add containing, starting, and ending counts per name. Doubles the cost.
UTC time the current index was built. Rebuilt after each daily zone sync.
total_domains
integer
Registered domains in the index.
total_slds
integer
Distinct second-level names.
total_tlds
integer
Extensions covered.
top_tlds[]
object[]
Largest 20 extensions by domain count.
POST/v1/rdap
2 credits per domain
Registration data
Resolves any extension through the IANA RDAP bootstrap, with WHOIS fallback where a registry has no RDAP service. Registries enforce hard global rate caps that every client shares, so this is the tightest limit on the platform. Registration data changes slowly: cache what you fetch.
Rate 6 requests/minCost 2 credits per domainBatch 10 domains
Shared upstream budget. Registries cap RDAP traffic globally. Cache results, and prefer /v1/dns or the zone index when you only need to know whether a name exists.
POST/v1/dns
1 credit per domain
DNS records
Resolves against public resolvers. Choose record types with record_types; the default set is A, AAAA, MX, TXT, NS.
Rate 12 requests/minCost 1 credit per domainBatch 50 domains
Records of that type. MX entries carry exchange and priority; SOA is an object or null.
results.<domain>.errors
string[]
Resolver errors for that domain, if any.
POST/v1/jobs
reserved at submit
Submit a job
Send the list once and let Toyroom do the batching. The body is a JSON object, or a plain text file with one name per line (Content-Type: text/plain, type and options in the query string), either optionally gzip-encoded (Content-Encoding: gzip, 32 MB max). Credits for the whole job are reserved when it is accepted and refunded for every row that is never processed. Give a callback_url to be told when it finishes; poll otherwise.
Rate 12 requests/minCost reserved at submit
Request
Field
Type
Description
type
string
required
zone_lookup, appraise, or categorize.
domains
string[]
required
Up to 1,000,000 names. Validated like the matching endpoint; the first bad entry rejects the whole submission.
options
object
optional
zone_lookup accepts include_counts (boolean). Other types take none.
callback_url
string
optional
HTTPS URL to POST a signed notice to when the job reaches a final state.
idempotency_key
string
optional
Resubmitting with the same key within 24 hours returns the existing job instead of creating another.
Rows written so far. Rows are written in input order as each chunk completes.
errors
integer
Rows the upstream tool could not answer after three attempts. They appear in the file with an error field and are refunded.
expires_at
string
When the results file is deleted: 24 hours after the job finishes.
callback
object
Delivery state of the callback: pending, retrying, delivered, or gave_up.
GET/v1/jobs/{job_id}/results
free
Download results
Streams the whole file. NDJSON is served gzip-encoded when the client accepts it. Add ?from=N to resume an NDJSON download at row N. A job that is still running or stopped early serves the rows it has, with X-Job-Partial: true. Files live 24 hours after the job finishes; after that the endpoint answers 410.
Names queued or running across your jobs, against the 5,000,000 cap.
DELETE/v1/jobs/{job_id}
free
Cancel or delete a job
A queued job is cancelled at once and fully refunded. A running job stops after its current chunk, keeps the rows it wrote (downloadable), and refunds the rest. A finished job has its file deleted now instead of at expiry.
The tool behind the endpoint did not answer. Safe to retry with backoff; nothing was charged.
{"error": "Appraisal service unavailable"}
MCP server
Every tool is also served over the Model Context Protocol (streamable HTTP) at https://api.toyroom.ai/mcp, so Claude, Cursor, or any MCP client can appraise and research domains directly. Same key, same quota: agent calls and REST calls draw from one pool. The server is stateless, so there is no session handshake.
Registered or not, and extension counts, for up to 500 names.
1 per 10 names
submit_job
Queue a zone_lookup, appraise, or categorize job for up to 50,000 names.
reserved at submit
job_status
Progress and status of a job.
free
job_results
A page of rows from a job, in input order.
free
rdap_lookup
Registration data for up to 5 domains.
2 per domain
dns_lookup
DNS records for up to 25 domains.
1 per domain
Tool batch limits are smaller than REST so a single agent turn stays fast. Lists over the limit are rejected with a message, never truncated.
Changelog
2026-09-16
New: Jobs. POST /v1/jobs takes up to 1,000,000 names for zone lookup, appraisal, or categorization; Toyroom batches, retries, and writes one results file kept 24 hours. Signed callbacks, polling, CSV or NDJSON download, cancel with refund.
New: POST /v1/zone/lookup, the bulk Zone Counter. Up to 5,000 names per request, registered or not plus extension counts, optional keyword counts. 1 credit per 10 names.
New MCP tools: zone_lookup (500 names), submit_job, job_status, job_results.
GET /v1/usage reports names in flight and results storage for jobs.
The Working at scale section now walks through a million-name run, as a job and as a paced loop.
2026-09-15
Stricter validation: every list item must be a string that looks like a domain; errors name the offending index.
Authentication now runs before the body is read. Unauthenticated requests get a 401 and nothing else.
Credits are refunded when an upstream tool fails (502). Rate and quota 429s carry a Retry-After header.
GET /v1/usage adds credits_remaining, resets_at, and the current limits.
Appraisal responses keep zone facts as counts; the per-extension list is no longer returned.
Machine-readable docs: /llms.txt, /llms-full.txt, /openapi.json, and Accept: text/markdown on this page.
MCP tools reject lists over their limit instead of silently truncating them.
2026-09-13
api.toyroom.ai opened to the public. Zone counts moved to the suffix-array index (sub-second cold queries).
2026-07-11
Private beta: appraisal, categorization, word splitting, zone counts, RDAP, DNS, MCP server.