Domain intelligence, as an API.
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.
Authentication
Every request carries your key in the Authorization header. Keys start with trm_live_ and are shown once at issue time. Toyroom stores only a hash.
# Every request, REST and MCP alike
Authorization: Bearer trm_live_your_key_hereCheck that a key works:
curl https://api.toyroom.ai/v1/ping \
-H "Authorization: Bearer $TOYROOM_KEY"
# {"ok": true, "key": "your-name"}Quickstart
Appraise a domain in one call. Pick your language once; the choice sticks for every example on this page.
curl https://api.toyroom.ai/v1/appraise \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "cloudkitchen.com"}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/appraise",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domain": "cloudkitchen.com"
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/appraise", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domain": "cloudkitchen.com"
}),
});
console.log(await r.json());{
"domain": "cloudkitchen.com",
"final": {
"value": 24852.56,
"low": 14564.18,
"high": 24852.56,
"version": "v2.1 (calibrated)"
},
"classification": {
"split": "Cloud Kitchen",
"vertical": "Food & Beverage",
"subcategory": "Delivery & Catering",
"domain_type": "industry",
"name_type": "words",
"language": "en"
},
"zone": {
"indexed": true,
"exact_count": 72,
"contains_count": 81050,
"contains_names": 61864
}
}Credits and limits
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.
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_quotafrom/v1/usage. Ask for a larger quota before you start, not after the 429.
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. Checkdaily_quotaon/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_urlwhen 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.
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"])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.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")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-Eventand aToyroom-Signatureheader (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, thencallback.statusreadsgave_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}.
Appraise a domain
Returns a calibrated USD estimate with a low and high range, the word split and industry classification that fed the valuation, how many extensions the same second-level name is registered under, and how many registered domains contain it.
Request
| Field | Type | Description | |
|---|---|---|---|
domain | string | required | One domain, e.g. cloudkitchen.com. |
Example
curl https://api.toyroom.ai/v1/appraise \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "cloudkitchen.com"}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/appraise",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domain": "cloudkitchen.com"
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/appraise", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domain": "cloudkitchen.com"
}),
});
console.log(await r.json());Response
{
"domain": "cloudkitchen.com",
"final": {
"value": 24852.56,
"low": 14564.18,
"high": 24852.56,
"version": "v2.1 (calibrated)"
},
"classification": {
"split": "Cloud Kitchen",
"vertical": "Food & Beverage",
"subcategory": "Delivery & Catering",
"domain_type": "industry",
"name_type": "words",
"language": "en"
},
"zone": {
"indexed": true,
"exact_count": 72,
"contains_count": 81050,
"contains_names": 61864
}
}Response fields
| Field | Type | Description |
|---|---|---|
final.value | number | USD estimate. |
final.low / final.high | number | Range around the estimate. |
final.version | string | Model version that produced the number. |
classification | object | Word split, vertical, subcategory, domain type, name type, language. |
classification.name_type | string or null | words (real words), brandable (a pronounceable coined name), junk (random letters), or idn (internationalized). Null when the older splitter handled the name. |
classification.language | string or null | Language the words were read in: en, es, pt, fr, de, it, nl, tr, id, or zh (pinyin). Null for brandable and junk names. |
zone.exact_count | integer | How many extensions this exact name is registered under, across the zone index. |
zone.contains_count | integer or null | How many registered domains contain the name anywhere (bakery: bakeryshop.com, mybakery.de). Null for names under 4 characters. |
zone.contains_names | integer or null | How many distinct second-level names contain it. |
Appraise a list
Same model as the single-domain call, one row per domain. Use it for portfolio triage.
Request
| Field | Type | Description | |
|---|---|---|---|
domains | string[] | required | Up to 500 domains. |
Example
curl https://api.toyroom.ai/v1/appraise/bulk \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["solarfarm.io", "aitrader.net"]}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/appraise/bulk",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domains": [
"solarfarm.io",
"aitrader.net"
]
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/appraise/bulk", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domains": [
"solarfarm.io",
"aitrader.net"
]
}),
});
console.log(await r.json());Response
{
"results": [
{
"domain": "solarfarm.io",
"value": 2033.72,
"low": 135.58,
"high": 2033.72,
"version": "v2.1 (calibrated)",
"classification": {
"split": "Solar Farm",
"vertical": "Industrial & Energy",
"subcategory": "Energy & Utilities",
"domain_type": "industry",
"name_type": "words",
"language": "en"
}
},
{
"domain": "aitrader.net",
"value": 329.12,
"low": 87.52,
"high": 4936.8,
"version": "v2.1 (calibrated)",
"classification": {
"split": "AI Trader",
"vertical": "Technology",
"subcategory": "AI & Machine Learning",
"domain_type": "industry",
"name_type": "words",
"language": "en"
}
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
results[] | object[] | One entry per input domain, in input order. |
results[].value / low / high | number | USD estimate and range. |
results[].classification | object | Same shape as the single-domain call. |
Categorize domains
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.
Request
| Field | Type | Description | |
|---|---|---|---|
domains | string[] | required | Up to 100 domains. |
Example
curl https://api.toyroom.ai/v1/categorize \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["cloudkitchen.com", "aitrader.io"]}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/categorize",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domains": [
"cloudkitchen.com",
"aitrader.io"
]
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/categorize", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domains": [
"cloudkitchen.com",
"aitrader.io"
]
}),
});
console.log(await r.json());Response
{
"results": [
{
"domain": "cloudkitchen.com",
"sld": "cloudkitchen",
"split": "Cloud Kitchen",
"vertical": "Food & Beverage",
"subcategory": "Delivery & Catering",
"confidence": "high",
"matched": [
"cloud",
"kitchen",
"cloudkitchen"
],
"source": "lexicon",
"type": "industry",
"name_type": "words",
"language": "en",
"tags": [
{
"vertical": "Technology",
"subcategory": "Data & Cloud"
}
]
}
],
"summary": {
"total": 2,
"categorized": 2,
"uncategorized": 0,
"verticals": [
{
"vertical": "Food & Beverage",
"count": 1
},
{
"vertical": "Technology",
"count": 1
}
]
}
}Response fields
| Field | Type | Description |
|---|---|---|
results[].vertical / subcategory | string | Primary classification. |
results[].confidence | string | high, medium, or low. |
results[].matched | string[] | Lexicon keywords that drove the result. |
results[].tags | object[] | Secondary vertical and subcategory pairs. |
results[].type | string | Domain type: industry, brandable, geo, personal, and so on. |
results[].name_type | string or null | What the name is made of: words, brandable, junk, or idn. Null when the older splitter handled the name. |
results[].language | string or null | Language the words were read in: en, es, pt, fr, de, it, nl, tr, id, or zh (pinyin). Null for brandable and junk names. |
summary | object | Counts by vertical, subcategory, and type for the whole batch. |
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.
Request
| Field | Type | Description | |
|---|---|---|---|
domains | string[] | required | Up to 1,000 domains. |
Example
curl https://api.toyroom.ai/v1/camelcase \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["cloudkitchen.com", "solarfarm.io"]}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/camelcase",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domains": [
"cloudkitchen.com",
"solarfarm.io"
]
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/camelcase", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domains": [
"cloudkitchen.com",
"solarfarm.io"
]
}),
});
console.log(await r.json());Response
{
"results": [
{
"original": "cloudkitchen.com",
"converted": "CloudKitchen.com",
"split": "Cloud Kitchen",
"word_count": 2,
"confidence": "HIGH",
"category": "Food & Beverage",
"subcategory": "Delivery & Catering",
"domain_type": "industry",
"name_type": "words",
"language": "en"
}
],
"word_stats": [
{
"word": "cloud",
"count": 1
},
{
"word": "kitchen",
"count": 1
}
],
"tld_stats": [
{
"tld": "com",
"count": 1
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
results[].converted | string | CamelCase form of the domain. |
results[].split | string | Space-separated words. |
results[].confidence | string | HIGH, MEDIUM, or LOW. |
results[].name_type | string or null | words (real words), brandable (a pronounceable coined name), junk (random letters), or idn (internationalized). Null when the older splitter handled the name. |
results[].language | string or null | Language the words were read in: en, es, pt, fr, de, it, nl, tr, id, or zh (pinyin). Null for brandable and junk names. |
word_stats / tld_stats | object[] | Batch-level frequency tables. |
Count a keyword
Searches the Toyroom zone index: 390M+ registered domains across 23,000+ extensions, rebuilt from daily zone-file snapshots. Returns totals and registered domains per extension across every match. Counts only: this endpoint never returns domain names, and that is a product boundary, not a missing feature.
Request
| Field | Type | Description | |
|---|---|---|---|
term | string | required | Keyword, 2 to 63 characters, letters, digits, and hyphens. |
tlds | string[] | optional | Count only these extensions, up to 50. Omit to count every extension. |
position | string | optional | contains (default), starts, or ends. |
Example
curl https://api.toyroom.ai/v1/zone/count \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"term": "solar", "tlds": ["com", "io"], "position": "contains"}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/zone/count",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"term": "solar",
"tlds": [
"com",
"io"
],
"position": "contains"
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/zone/count", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"term": "solar",
"tlds": [
"com",
"io"
],
"position": "contains"
}),
});
console.log(await r.json());Response
{
"term": "solar",
"position": "contains",
"tlds": [
"com",
"io"
],
"total_matches": 156704,
"tld_match_count": 9432,
"exact_domain_count": 2,
"keyword_count": 155748,
"tld_breakdown": [
{
"tld": "com",
"count": 155701
},
{
"tld": "io",
"count": 1003
}
],
"total_domains_in_db": 391652800
}Response fields
| Field | Type | Description |
|---|---|---|
total_matches | integer | 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. |
tlds | string[] or null | The extensions counted, as requested. Null when every extension was counted. |
tld_breakdown[] | object[] | Registered domains per extension across every match, largest first: each requested extension, or the 20 largest when tlds is omitted. |
total_domains_in_db | integer | Size of the index the counts were taken from. |
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.
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. |
Example
curl https://api.toyroom.ai/v1/zone/lookup \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["cloudkitchen.com", "solarfarm.io", "zqxjv.net"], "include_counts": false}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/zone/lookup",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domains": [
"cloudkitchen.com",
"solarfarm.io",
"zqxjv.net"
],
"include_counts": false
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/zone/lookup", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domains": [
"cloudkitchen.com",
"solarfarm.io",
"zqxjv.net"
],
"include_counts": false
}),
});
console.log(await r.json());Response
{
"results": [
{
"input": "cloudkitchen.com",
"name": "cloudkitchen",
"extension": "com",
"registered": true,
"zone_complete": true,
"extensions_registered": 72
},
{
"input": "solarfarm.io",
"name": "solarfarm",
"extension": "io",
"registered": true,
"zone_complete": false,
"extensions_registered": 41
},
{
"input": "zqxjv.net",
"name": "zqxjv",
"extension": "net",
"registered": false,
"zone_complete": true,
"extensions_registered": 0
}
],
"index_built_at": "2026-09-15T04:27:31Z"
}Response fields
| Field | Type | Description |
|---|---|---|
results[].registered | boolean or null | True when the exact domain is in the index. False only for extensions with a complete zone file (zone_complete). Null when the input had no extension, or the name is missing from an extension the index sees only in part. |
results[].zone_complete | boolean or null | True for gTLDs, read from daily zone files: a missing name is not registered there. False for country codes (.io, .ai, .co.uk) and IDN extensions, which the index sees only in part. |
results[].extensions_registered | integer | How many extensions the name is registered under, across the whole index. |
results[].containing_domains | integer | With include_counts: registered domains whose name contains this name. |
results[].containing_names | integer | With include_counts: distinct names that contain this name. |
results[].starting_domains / ending_domains | integer | With include_counts: registered domains whose name starts or ends with this name. |
results[].supported | boolean | False for names under 2 characters; their counts are null. |
index_built_at | string | UTC build time of the index the answers came from. |
Index snapshot
Use it to cite the exact index size and build time next to any count you publish.
Example
curl https://api.toyroom.ai/v1/zone/stats \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.get("https://api.toyroom.ai/v1/zone/stats",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/zone/stats", {
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"built_at": "2026-09-15T04:27:31Z",
"total_domains": 391652800,
"total_slds": 256425219,
"total_tlds": 23533,
"top_tlds": [
{
"tld": "com",
"count": 167894670
},
{
"tld": "de",
"count": 17895018
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
built_at | string | 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. |
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.
Request
| Field | Type | Description | |
|---|---|---|---|
domains | string[] | required | Up to 10 domains. |
Example
curl https://api.toyroom.ai/v1/rdap \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["google.com"]}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/rdap",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domains": [
"google.com"
]
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/rdap", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domains": [
"google.com"
]
}),
});
console.log(await r.json());Response
{
"results": [
{
"domain": "google.com",
"status": "found",
"registrationDate": "1997-09-15T04:00:00.000Z",
"expirationDate": "2028-09-14T04:00:00.000Z",
"lastUpdatedDate": "2026-09-15T18:23:54.000Z",
"registrar": "MarkMonitor Inc.",
"age": "29 years",
"statusFlags": [
"client delete prohibited",
"client transfer prohibited"
],
"nameservers": [
"NS1.GOOGLE.COM",
"NS2.GOOGLE.COM"
],
"dnssecEnabled": false
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
results[].status | string | found, not_found, or error. |
results[].registrationDate / expirationDate | string | ISO 8601, UTC. |
results[].registrar | string | Sponsoring registrar. |
results[].statusFlags | string[] | EPP status codes, lower-cased. |
results[].nameservers | string[] | Delegated nameservers. |
/v1/dns or the zone index when you only need to know whether a name exists.DNS records
Resolves against public resolvers. Choose record types with record_types; the default set is A, AAAA, MX, TXT, NS.
Request
| Field | Type | Description | |
|---|---|---|---|
domains | string[] | required | Up to 50 domains. |
record_types | string[] | optional | Any of A, AAAA, MX, TXT, NS, CNAME, SOA. |
Example
curl https://api.toyroom.ai/v1/dns \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["google.com"], "record_types": ["A", "MX"]}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/dns",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"domains": [
"google.com"
],
"record_types": [
"A",
"MX"
]
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/dns", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"domains": [
"google.com"
],
"record_types": [
"A",
"MX"
]
}),
});
console.log(await r.json());Response
{
"results": {
"google.com": {
"A": [
"142.251.14.102",
"142.251.14.101"
],
"MX": [
{
"exchange": "smtp.google.com",
"priority": 10
}
],
"errors": []
}
}
}Response fields
| Field | Type | Description |
|---|---|---|
results.<domain> | object | One key per input domain. |
results.<domain>.<TYPE> | array | 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. |
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.
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. |
Example
curl https://api.toyroom.ai/v1/jobs \
-H "Authorization: Bearer $TOYROOM_KEY" \
-H "Content-Type: application/json" \
-d '{"type": "zone_lookup", "options": {"include_counts": false}, "domains": ["cloudkitchen.com", "solarfarm.io", "zqxjv.net"], "callback_url": "https://client.example/hooks/toyroom", "idempotency_key": "portfolio-2026-09-16"}'import os, requests
r = requests.post("https://api.toyroom.ai/v1/jobs",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"},
json={
"type": "zone_lookup",
"options": {
"include_counts": false
},
"domains": [
"cloudkitchen.com",
"solarfarm.io",
"zqxjv.net"
],
"callback_url": "https://client.example/hooks/toyroom",
"idempotency_key": "portfolio-2026-09-16"
})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOYROOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"type": "zone_lookup",
"options": {
"include_counts": false
},
"domains": [
"cloudkitchen.com",
"solarfarm.io",
"zqxjv.net"
],
"callback_url": "https://client.example/hooks/toyroom",
"idempotency_key": "portfolio-2026-09-16"
}),
});
console.log(await r.json());Response
{
"id": "job_9f1c2a7d0b3e4f5a6b7c8d9e",
"type": "zone_lookup",
"status": "queued",
"total": 3,
"done": 0,
"errors": 0,
"progress": 0.0,
"options": {
"include_counts": false
},
"credits_reserved": 1,
"credits_refunded": 0,
"credits_charged": 1,
"position": 1,
"created": "2026-09-16T10:03:12Z",
"estimated_start": "2026-09-16T10:03:12Z",
"estimated_finish": "2026-09-16T10:03:13Z",
"deadline": "2026-09-16T12:03:12Z",
"started": null,
"finished": null,
"expires_at": null,
"partial": false,
"poll_url": "/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e",
"results_url": null,
"callback": {
"url": "https://client.example/hooks/toyroom",
"status": "pending",
"attempts": 0,
"last_error": null
}
}Response fields
| Field | Type | Description |
|---|---|---|
status | string | queued, running, succeeded, partial, failed, expired, or cancelled. |
position | integer | Place in line while queued; 1 is next. |
estimated_start / estimated_finish | string | From the lane's measured throughput and the names ahead of you. |
deadline | string | If the job is still queued at this time it expires and every credit is refunded. |
credits_reserved | integer | Charged now against today's quota. |
poll_url / results_url | string | Where to check status, and where the file appears once rows exist. |
Check a job
Free and not rate limited. While queued or running the response carries Retry-After: 30, a sensible polling interval.
Example
curl https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.get("https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e", {
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"id": "job_9f1c2a7d0b3e4f5a6b7c8d9e",
"type": "zone_lookup",
"status": "running",
"total": 1000000,
"done": 415000,
"errors": 0,
"progress": 0.415,
"options": {
"include_counts": false
},
"credits_reserved": 100000,
"credits_refunded": 0,
"credits_charged": 100000,
"created": "2026-09-16T10:03:12Z",
"estimated_start": null,
"estimated_finish": "2026-09-16T10:20:41Z",
"deadline": null,
"started": "2026-09-16T10:04:02Z",
"finished": null,
"expires_at": null,
"partial": false,
"poll_url": "/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e",
"results_url": null
}Response fields
| Field | Type | Description |
|---|---|---|
done / total / progress | integer, number | 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. |
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.
Example
curl https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e/results \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.get("https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e/results",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e/results", {
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"input": "cloudkitchen.com",
"name": "cloudkitchen",
"extension": "com",
"registered": true,
"extensions_registered": 72,
"supported": true
}Response fields
| Field | Type | Description |
|---|---|---|
(each line) | object | One JSON object per input name, in input order, with the same fields as the matching synchronous endpoint. A row that failed carries an error field. |
X-Job-Status, X-Job-Rows, X-Job-Partial | headers | Status at download time, rows in the file, and whether the file is incomplete. |
List jobs
Newest first. Expired jobs stay listed for a day after their files are removed.
Example
curl https://api.toyroom.ai/v1/jobs \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.get("https://api.toyroom.ai/v1/jobs",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/jobs", {
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"jobs": [
{
"id": "job_9f1c2a7d0b3e4f5a6b7c8d9e",
"type": "zone_lookup",
"status": "succeeded",
"total": 1000000,
"done": 1000000,
"errors": 0,
"progress": 1.0,
"results_url": "/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e/results",
"expires_at": "2026-09-17T10:20:41Z"
}
],
"names_in_flight": 0
}Response fields
| Field | Type | Description |
|---|---|---|
jobs[] | object[] | Same shape as the single-job status. |
names_in_flight | integer | Names queued or running across your jobs, against the 5,000,000 cap. |
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.
Example
curl -X DELETE https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.delete("https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e", {
method: "DELETE",
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"id": "job_9f1c2a7d0b3e4f5a6b7c8d9e",
"type": "zone_lookup",
"status": "cancelled",
"total": 1000000,
"done": 415000,
"errors": 0,
"progress": 0.415,
"credits_reserved": 100000,
"credits_refunded": 58500,
"credits_charged": 41500,
"results_url": "/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e/results"
}Response fields
| Field | Type | Description |
|---|---|---|
status | string | cancelled, or expired when a finished job's file was removed. |
credits_refunded | integer | Credits returned for rows never processed. |
Usage today
Free to call. Poll it to pace a long batch or to show a balance in your own tooling.
Example
curl https://api.toyroom.ai/v1/usage \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.get("https://api.toyroom.ai/v1/usage",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/usage", {
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"key": "your-name",
"date": "2026-09-15",
"credits_used": 16,
"credits_remaining": 1984,
"daily_quota": 2000,
"resets_at": "2026-09-16T00:00:00Z",
"by_endpoint": {
"appraise": 2,
"zone": 6,
"categorize": 4,
"rdap": 2,
"dns": 1,
"camelcase": 1
},
"rate_limits_per_minute": {
"appraise": 30,
"appraise_bulk": 6,
"categorize": 12,
"camelcase": 60,
"zone": 20,
"zone_lookup": 12,
"jobs": 12,
"rdap": 6,
"dns": 12
},
"batch_limits": {
"appraise_bulk": 500,
"categorize": 100,
"camelcase": 1000,
"zone_lookup": 5000,
"rdap": 10,
"dns": 50
}
}Response fields
| Field | Type | Description |
|---|---|---|
credits_used / credits_remaining | integer | Against today's quota, UTC day. |
resets_at | string | Next quota reset, ISO 8601. |
by_endpoint | object | Credits by rate bucket. |
rate_limits_per_minute | object | Requests per minute per bucket. |
batch_limits | object | Maximum list length per endpoint. |
Check a key
Free to call and never rate limited. Use it in health checks.
Example
curl https://api.toyroom.ai/v1/ping \
-H "Authorization: Bearer $TOYROOM_KEY"import os, requests
r = requests.get("https://api.toyroom.ai/v1/ping",
headers={"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"})
r.raise_for_status()
print(r.json())const r = await fetch("https://api.toyroom.ai/v1/ping", {
headers: { Authorization: `Bearer ${process.env.TOYROOM_KEY}` },
});
console.log(await r.json());Response
{
"ok": true,
"key": "your-name"
}Response fields
| Field | Type | Description |
|---|---|---|
ok | boolean | Always true on a 200. |
key | string | The name the key was issued under. |
Errors
Every error is JSON with an error string that says what to fix. 429 responses carry a Retry-After header in seconds.
| Status | Meaning | What to do | Body |
|---|---|---|---|
400 | Bad request | The body is not valid JSON, a required field is missing, a list is too long, or an item is not a string. The message names the field. | {"error": "Body must include 'domains': [\"a.com\", ...] with at most 500 items"} |
401 | Missing or invalid key | No Bearer token, an unknown key, or a revoked key. The body never parses before authentication. | {"error": "Missing or invalid API key. Send: Authorization: Bearer trm_..."} |
429 | Rate limit | Too many requests to one bucket in the current minute. Wait for Retry-After seconds. | {"error": "Rate limit exceeded for rdap (6/min). Slow down and retry.", "retry_after_seconds": 30} |
429 | Not enough credits | The request costs more than today's remaining credits. A smaller batch goes through now; the same request goes through after Retry-After, at 00:00 UTC. | {"error": "Not enough credits: this request costs 200 and 62 remain in today's quota of 2,000. Send a smaller batch, or retry after the reset at 00:00 UTC.", "code": "quota", "credits_needed": 200, "credits_remaining": 62, "resets_at": "2026-09-16T00:00:00Z"} |
429 | Daily quota | Today's credits are spent. Retry-After counts down to 00:00 UTC. | {"error": "Daily quota exhausted (2,000/2,000 credits). Resets at 00:00 UTC.", "code": "quota", "credits_needed": 1, "credits_remaining": 0, "resets_at": "2026-09-16T00:00:00Z"} |
502 | Upstream unavailable | The tool behind the endpoint did not answer. Safe to retry with backoff; nothing was charged. | {"error": "Appraisal service unavailable"} |
Use in Claude, no key
The research tools also run as a free MCP connector at https://api.toyroom.ai/mcp/open. No key and no sign-in. Ask Claude about a name and it appraises it, sorts it by industry, and checks the zone files. Every tool is read-only.
Claude on the web, desktop, and mobile
Add Toyroom to Claude, or open Settings, then Connectors, then Add custom connector, and paste the URL. On Team and Enterprise plans an Owner adds it for the organization.
Claude Code
claude mcp add toyroom --scope user --transport http https://api.toyroom.ai/mcp/openAny MCP client
{
"mcpServers": {
"toyroom": {
"type": "http",
"url": "https://api.toyroom.ai/mcp/open"
}
}
}Try asking
- What is bakery.ai worth? Is "bakery" registered in other extensions too?
- I am naming a solar installer in Austin. Suggest ten short .com names, check which are already registered, and appraise the rest.
- How crowded is the keyword "robot" compared with "droid"? Count registered domains for each, overall and in .com and .ai.
- Sort these by industry: tacoloco.com, coffeebeans.shop, petvet.ai, lawfirm.io, cloudbackup.io, ledgerly.io.
Tools
| Tool | Does | Credits |
|---|---|---|
appraise_domain | Value estimate for one domain. | 1 |
appraise_bulk | Value estimates for up to 25 domains. | 1 per domain |
categorize_domains | Verticals and subcategories for up to 25 domains. | 2 per domain |
zone_count | Keyword counts across 390M+ registered domains, for keywords of 3+ characters. | 2 |
zone_lookup | Registered or not, and extension counts, for up to 100 names (10 with keyword counts). | 1 per 10 names |
Each caller gets 200 credits a day: one IPv4 address, or one IPv6 /64, with 2,000 across an IPv6 /48. Claude's hosted apps call from one shared address range and share 100,000 credits a day of their own. All other keyless callers together stop at 50,000 credits a day. Each tool family also has its own daily ceiling, so one busy tool leaves the others working. Credits cost the same as with a key and reset at 00:00 UTC. Calls that fail still count. Keyword counts on zone_lookup take names of {C.OPEN_MCP_COUNTS_MIN_NAME}+ characters, 1 credit each. For more, use the keyed server.
What Toyroom keeps from these calls is in the privacy policy.
MCP server
Appraisal, classification, zone, job, RDAP, and DNS tools are 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. Word splitting, zone stats, usage, and job list and cancel are REST only. Same key, same quota: agent calls and REST calls draw from one pool. The server is stateless, so there is no session handshake.
Claude Code
# --scope user: available in every project, not just this directory
claude mcp add toyroom --scope user --transport http https://api.toyroom.ai/mcp \
--header "Authorization: Bearer trm_live_your_key_here"Any MCP client
{
"mcpServers": {
"toyroom": {
"type": "http",
"url": "https://api.toyroom.ai/mcp",
"headers": {
"Authorization": "Bearer trm_live_your_key_here"
}
}
}
}Claude Desktop
Claude Desktop, and any client with no setting for request headers, connects through the mcp-remote bridge. It needs Node.js. Add this to claude_desktop_config.json and restart the app.
{
"mcpServers": {
"toyroom": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.toyroom.ai/mcp",
"--header",
"Authorization:${TOYROOM_AUTH}"
],
"env": {
"TOYROOM_AUTH": "Bearer trm_live_your_key_here"
}
}
}
}Tools
| Tool | Does | Credits |
|---|---|---|
appraise_domain | Value estimate for one domain. | 1 |
appraise_bulk | Value estimates for up to 100 domains. | 1 per domain |
categorize_domains | Verticals and subcategories for up to 50 domains. | 2 per domain |
zone_count | Keyword counts across 390M+ registered domains. | 2 |
zone_lookup | 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-10-03
- Appraisals add zone.contains_count and zone.contains_names: how many registered domains, and distinct names, contain the name (names of 4+ characters). REST and MCP, and the appraisal card.
- New: a free MCP connector at /mcp/open. No key, no sign-in. Five read-only research tools with smaller batches and a daily allowance.
- Every MCP tool carries a title and read-only or write annotations, so clients can run read-only tools without asking each time.
- Appraisal, categorization, and zone tools render as cards in Claude and other MCP Apps hosts: estimates with their range, ranked lists, registration checks, and keyword counts, in light and dark.
- Both MCP servers speak protocol 2026-07-28 (the stateless core) as well as 2025-11-25 and earlier.
- Fix: zone count tld_breakdown (REST and MCP) now counts every match: the 20 largest extensions, or every extension you pass in tlds, zeros included. Without tlds it used to come from the first 100 matching names. Responses also echo tlds.
- Zone lookup rows gain zone_complete. registered is now null, not false, when a name is missing from a country-code or IDN extension the index sees only in part (REST, MCP, and the last column of job CSVs).
- 2026-09-29
- A request that costs more than your remaining credits now says so, with credits_needed and credits_remaining in the body. It used to report the quota as exhausted while credits were left.
- MCP tools validate input before charging, refund when the tool behind them fails, and report failures as tool errors (isError) instead of results.
- GET /mcp answers 405: the server is stateless and never opens a standalone event stream.
- Job results always carry X-Job-Partial, true or false.
- MCP setup for Claude Desktop through mcp-remote.
- 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.