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"
  },
  "zone": {
    "indexed": true,
    "exact_count": 72
  }
}

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, and how many extensions the same second-level name is registered under.

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"
  },
  "zone": {
    "indexed": true,
    "exact_count": 72
  }
}

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.
zone.exact_countintegerHow many extensions this exact name is registered under, across the zone index.
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"
      }
    },
    {
      "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"
      }
    }
  ]
}

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",
      "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.
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"
    }
  ],
  "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.
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 a per-extension breakdown. Counts only: this endpoint never returns domain names, and that is a product boundary, not a missing feature.

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

Request

FieldTypeDescription
termstringrequiredKeyword, 2 to 63 characters, letters, digits, and hyphens.
tldsstring[]optionalRestrict the breakdown to these extensions, up to 50. Omit for all.
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",
  "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.
tld_breakdown[]object[]Count per extension, largest first.
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,
      "extensions_registered": 72
    },
    {
      "input": "solarfarm.io",
      "name": "solarfarm",
      "extension": "io",
      "registered": true,
      "extensions_registered": 41
    },
    {
      "input": "zqxjv.net",
      "name": "zqxjv",
      "extension": "net",
      "registered": false,
      "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. Null when the input had no extension.
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}
429Daily quotaToday's credits are spent. Retry-After counts down to 00:00 UTC.{"error": "Daily quota exhausted (2000/2000 credits). Resets at 00:00 UTC.", "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"}

MCP server

Every tool is also served over the Model Context Protocol (streamable HTTP) at https://api.toyroom.ai/mcp, so Claude, Cursor, or any MCP client can appraise and research domains directly. Same key, same quota: agent calls and REST calls draw from one pool. The server is stateless, so there is no session handshake.

Claude Code

shell
claude mcp add toyroom --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"
      }
    }
  }
}

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-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.