Toyroom
Explore Toyroom
Developer documentation · v1 beta

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.

390M+registered domains indexed
23,000+extensions covered
16REST endpoints
10MCP tools
Base URLhttps://api.toyroom.ai AuthAuthorization: Bearer trm_live_… For agents/llms.txt · /llms-full.txt · /openapi.json · /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.

header
# Every request, REST and MCP alike
Authorization: Bearer trm_live_your_key_here

Check that a key works:

curl
curl https://api.toyroom.ai/v1/ping \
  -H "Authorization: Bearer $TOYROOM_KEY"
# {"ok": true, "key": "your-name"}
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.

curl https://api.toyroom.ai/v1/appraise \
  -H "Authorization: Bearer $TOYROOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "cloudkitchen.com"}'
200 · application/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.

EndpointCreditsRate limitBatch max
/v1/appraise1 credit30/min
/v1/appraise/bulk1 credit per domain6/min500 domains
/v1/categorize2 credits per domain12/min100 domains
/v1/camelcase1 credit60/min1,000 domains
/v1/zone/count2 credits20/min50 extensions
/v1/zone/lookup1 credit per 10 names12/min5,000 domains
/v1/zone/stats1 credit20/min
/v1/rdap2 credits per domain6/min10 domains
/v1/dns1 credit per domain12/min50 domains
/v1/jobsreserved at submit12/min
/v1/jobs/{job_id}freenone
/v1/jobs/{job_id}/resultsfreenone
/v1/jobsfreenone
/v1/jobs/{job_id}freenone
/v1/usagefreenone
/v1/pingfreenone

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:

EndpointPer requestPer minutePer hourCredits
/v1/appraise/bulk5003,000180,0001 credit per domain
/v1/categorize1001,20072,0002 credits per domain
/v1/camelcase1,00060,0003.6M1 credit per request
/v1/zone/lookup5,00060,0003.6M1 credit per 10 names
/v1/zone/count1 term201,2002 credits per term
/v1/dns5060036,0001 credit per domain
/v1/rdap10603,6002 credits per domain

Three rules make a long run boring in the good way:

  1. Pace, do not poll. A 429 costs a round trip and nothing else, but sleeping between batches is cheaper than bouncing off the limit.
  2. Retry 502 with backoff. A 502 means the tool behind the endpoint did not answer and no credits were charged. Retry the same batch.
  3. 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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

TypePer nameCreditsThroughput
zone_lookupRegistered or not and extension counts per name; include_counts adds keyword counts1 credit per 10 names (doubled with counts)about 20,000 names per second
appraisev2.1 value estimate per domain1 credit per domainabout 1,800 domains per second
categorizeIndustry vertical and subcategory per domain2 credits per domainabout 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, how many extensions the same second-level name is registered under, and how many registered domains contain it.

Rate 30 requests/minCost 1 credit

Request

FieldTypeDescription
domainstringrequiredOne 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"}'

Response

200 · application/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
  }
}

Response fields

FieldTypeDescription
final.valuenumberUSD estimate.
final.low / final.highnumberRange around the estimate.
final.versionstringModel version that produced the number.
classificationobjectWord split, vertical, subcategory, domain type, name type, language.
classification.name_typestring or nullwords (real words), brandable (a pronounceable coined name), junk (random letters), or idn (internationalized). Null when the older splitter handled the name.
classification.languagestring or nullLanguage 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_countintegerHow many extensions this exact name is registered under, across the zone index.
zone.contains_countinteger or nullHow many registered domains contain the name anywhere (bakery: bakeryshop.com, mybakery.de). Null for names under 4 characters.
zone.contains_namesinteger or nullHow many distinct second-level names contain it.
POST/v1/appraise/bulk
1 credit per domain

Appraise a list

Same model as the single-domain call, one row per domain. Use it for portfolio triage.

Rate 6 requests/minCost 1 credit per domainBatch 500 domains

Request

FieldTypeDescription
domainsstring[]requiredUp 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"]}'

Response

200 · application/json
{
  "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

FieldTypeDescription
results[]object[]One entry per input domain, in input order.
results[].value / low / highnumberUSD estimate and range.
results[].classificationobjectSame shape as the single-domain call.
POST/v1/categorize
2 credits per domain

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.

Rate 12 requests/minCost 2 credits per domainBatch 100 domains

Request

FieldTypeDescription
domainsstring[]requiredUp 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"]}'

Response

200 · application/json
{
  "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

FieldTypeDescription
results[].vertical / subcategorystringPrimary classification.
results[].confidencestringhigh, medium, or low.
results[].matchedstring[]Lexicon keywords that drove the result.
results[].tagsobject[]Secondary vertical and subcategory pairs.
results[].typestringDomain type: industry, brandable, geo, personal, and so on.
results[].name_typestring or nullWhat the name is made of: words, brandable, junk, or idn. Null when the older splitter handled the name.
results[].languagestring or nullLanguage the words were read in: en, es, pt, fr, de, it, nl, tr, id, or zh (pinyin). Null for brandable and junk names.
summaryobjectCounts 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.

Rate 60 requests/minCost 1 creditBatch 1,000 domains

Request

FieldTypeDescription
domainsstring[]requiredUp 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"]}'

Response

200 · application/json
{
  "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

FieldTypeDescription
results[].convertedstringCamelCase form of the domain.
results[].splitstringSpace-separated words.
results[].confidencestringHIGH, MEDIUM, or LOW.
results[].name_typestring or nullwords (real words), brandable (a pronounceable coined name), junk (random letters), or idn (internationalized). Null when the older splitter handled the name.
results[].languagestring or nullLanguage 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_statsobject[]Batch-level frequency tables.
POST/v1/zone/count
2 credits

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.

Rate 20 requests/minCost 2 creditsExtensions up to 50 per query

Request

FieldTypeDescription
termstringrequiredKeyword, 2 to 63 characters, letters, digits, and hyphens.
tldsstring[]optionalCount only these extensions, up to 50. Omit to count every extension.
positionstringoptionalcontains (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"}'

Response

200 · application/json
{
  "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

FieldTypeDescription
total_matchesintegerDomains whose second-level name matches, within the requested extensions.
exact_domain_countintegerDomains whose second-level name is exactly the term.
keyword_countintegerDistinct second-level names that match.
tld_match_countintegerMatches where the term also appears in the extension.
tldsstring[] or nullThe 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_dbintegerSize 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

FieldTypeDescription
domainsstring[]requiredUp to 5,000 domains or bare names. The first label is the name, the rest is the extension.
include_countsbooleanoptionalAdd 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}'

Response

200 · application/json
{
  "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

FieldTypeDescription
results[].registeredboolean or nullTrue 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_completeboolean or nullTrue 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_registeredintegerHow many extensions the name is registered under, across the whole index.
results[].containing_domainsintegerWith include_counts: registered domains whose name contains this name.
results[].containing_namesintegerWith include_counts: distinct names that contain this name.
results[].starting_domains / ending_domainsintegerWith include_counts: registered domains whose name starts or ends with this name.
results[].supportedbooleanFalse for names under 2 characters; their counts are null.
index_built_atstringUTC build time of the index the answers came from.
GET/v1/zone/stats
1 credit

Index snapshot

Use it to cite the exact index size and build time next to any count you publish.

Rate 20 requests/minCost 1 credit

Example

curl https://api.toyroom.ai/v1/zone/stats \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "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

FieldTypeDescription
built_atstringUTC time the current index was built. Rebuilt after each daily zone sync.
total_domainsintegerRegistered domains in the index.
total_sldsintegerDistinct second-level names.
total_tldsintegerExtensions 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

Request

FieldTypeDescription
domainsstring[]requiredUp 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"]}'

Response

200 · application/json
{
  "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

FieldTypeDescription
results[].statusstringfound, not_found, or error.
results[].registrationDate / expirationDatestringISO 8601, UTC.
results[].registrarstringSponsoring registrar.
results[].statusFlagsstring[]EPP status codes, lower-cased.
results[].nameserversstring[]Delegated nameservers.
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

Request

FieldTypeDescription
domainsstring[]requiredUp to 50 domains.
record_typesstring[]optionalAny 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"]}'

Response

200 · application/json
{
  "results": {
    "google.com": {
      "A": [
        "142.251.14.102",
        "142.251.14.101"
      ],
      "MX": [
        {
          "exchange": "smtp.google.com",
          "priority": 10
        }
      ],
      "errors": []
    }
  }
}

Response fields

FieldTypeDescription
results.<domain>objectOne key per input domain.
results.<domain>.<TYPE>arrayRecords of that type. MX entries carry exchange and priority; SOA is an object or null.
results.<domain>.errorsstring[]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

FieldTypeDescription
typestringrequiredzone_lookup, appraise, or categorize.
domainsstring[]requiredUp to 1,000,000 names. Validated like the matching endpoint; the first bad entry rejects the whole submission.
optionsobjectoptionalzone_lookup accepts include_counts (boolean). Other types take none.
callback_urlstringoptionalHTTPS URL to POST a signed notice to when the job reaches a final state.
idempotency_keystringoptionalResubmitting 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"}'

Response

200 · application/json
{
  "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

FieldTypeDescription
statusstringqueued, running, succeeded, partial, failed, expired, or cancelled.
positionintegerPlace in line while queued; 1 is next.
estimated_start / estimated_finishstringFrom the lane's measured throughput and the names ahead of you.
deadlinestringIf the job is still queued at this time it expires and every credit is refunded.
credits_reservedintegerCharged now against today's quota.
poll_url / results_urlstringWhere to check status, and where the file appears once rows exist.
GET/v1/jobs/{job_id}
free

Check a job

Free and not rate limited. While queued or running the response carries Retry-After: 30, a sensible polling interval.

Rate not rate limitedCost free

Example

curl https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "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

FieldTypeDescription
done / total / progressinteger, numberRows written so far. Rows are written in input order as each chunk completes.
errorsintegerRows the upstream tool could not answer after three attempts. They appear in the file with an error field and are refunded.
expires_atstringWhen the results file is deleted: 24 hours after the job finishes.
callbackobjectDelivery 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.

Rate not rate limitedCost free

Example

curl https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e/results \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "input": "cloudkitchen.com",
  "name": "cloudkitchen",
  "extension": "com",
  "registered": true,
  "extensions_registered": 72,
  "supported": true
}

Response fields

FieldTypeDescription
(each line)objectOne 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-PartialheadersStatus at download time, rows in the file, and whether the file is incomplete.
GET/v1/jobs
free

List jobs

Newest first. Expired jobs stay listed for a day after their files are removed.

Rate not rate limitedCost free

Example

curl https://api.toyroom.ai/v1/jobs \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "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

FieldTypeDescription
jobs[]object[]Same shape as the single-job status.
names_in_flightintegerNames 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.

Rate not rate limitedCost free

Example

curl -X DELETE https://api.toyroom.ai/v1/jobs/job_9f1c2a7d0b3e4f5a6b7c8d9e \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "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

FieldTypeDescription
statusstringcancelled, or expired when a finished job's file was removed.
credits_refundedintegerCredits returned for rows never processed.
GET/v1/usage
free

Usage today

Free to call. Poll it to pace a long batch or to show a balance in your own tooling.

Rate not rate limitedCost free

Example

curl https://api.toyroom.ai/v1/usage \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "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

FieldTypeDescription
credits_used / credits_remainingintegerAgainst today's quota, UTC day.
resets_atstringNext quota reset, ISO 8601.
by_endpointobjectCredits by rate bucket.
rate_limits_per_minuteobjectRequests per minute per bucket.
batch_limitsobjectMaximum list length per endpoint.
GET/v1/ping
free

Check a key

Free to call and never rate limited. Use it in health checks.

Rate not rate limitedCost free

Example

curl https://api.toyroom.ai/v1/ping \
  -H "Authorization: Bearer $TOYROOM_KEY"

Response

200 · application/json
{
  "ok": true,
  "key": "your-name"
}

Response fields

FieldTypeDescription
okbooleanAlways true on a 200.
keystringThe 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.

StatusMeaningWhat to doBody
400Bad requestThe 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"}
401Missing or invalid keyNo 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_..."}
429Rate limitToo 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}
429Not enough creditsThe 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"}
429Daily quotaToday'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"}
502Upstream unavailableThe 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

shell
claude mcp add toyroom --scope user --transport http https://api.toyroom.ai/mcp/open

Any MCP client

json
{
  "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

ToolDoesCredits
appraise_domainValue estimate for one domain.1
appraise_bulkValue estimates for up to 25 domains.1 per domain
categorize_domainsVerticals and subcategories for up to 25 domains.2 per domain
zone_countKeyword counts across 390M+ registered domains, for keywords of 3+ characters.2
zone_lookupRegistered 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.

Registered means present in a zone file. It is not a live availability check, and a registered name can be missing from DNS. Zone tools return counts and booleans, never lists of domains.

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

shell
# --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

json
{
  "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.

json
{
  "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

ToolDoesCredits
appraise_domainValue estimate for one domain.1
appraise_bulkValue estimates for up to 100 domains.1 per domain
categorize_domainsVerticals and subcategories for up to 50 domains.2 per domain
zone_countKeyword counts across 390M+ registered domains.2
zone_lookupRegistered or not, and extension counts, for up to 500 names.1 per 10 names
submit_jobQueue a zone_lookup, appraise, or categorize job for up to 50,000 names.reserved at submit
job_statusProgress and status of a job.free
job_resultsA page of rows from a job, in input order.free
rdap_lookupRegistration data for up to 5 domains.2 per domain
dns_lookupDNS 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.