# Toyroom API reference

> Domain intelligence from toyroom.ai as an API: ML domain appraisal, industry classification, word splitting, keyword counts across 390M+ registered domains, RDAP registration data, and DNS records. REST at https://api.toyroom.ai/v1 and MCP at https://api.toyroom.ai/mcp share one key and one daily credit quota.

Base URL: https://api.toyroom.ai  ·  Version: v1 (beta)  ·  Contact: feedback@toyroom.ai  ·  OpenAPI: https://api.toyroom.ai/openapi.json

## Rules for agents

1. Send `Authorization: Bearer trm_live_...` on every request. Never put the key in a URL.
2. All POST bodies are JSON objects. Lists must contain strings that look like domains (letters, digits, hyphens, dots).
3. Respect batch limits (below). Oversized lists return 400; split them.
4. On 429 read the `Retry-After` header (seconds) and wait. Rate 429s are per endpoint per minute; quota 429s last until 00:00 UTC.
5. On 502 nothing was charged. Retry the same request with backoff.
6. `GET /v1/usage` is free: call it to see credits_remaining before a long run.
7. Zone endpoints return counts and booleans only, never domain names. Do not try to enumerate names through them.
7b. For a list of names use POST /v1/zone/lookup (5,000 per request). POST /v1/zone/count is one keyword per request.
9. For lists over 5,000 names, or any list you do not want to babysit, submit a job (POST /v1/jobs) and poll or wait for the callback; then download the file. Do not loop the synchronous endpoints for a million names when a job will do.
8. Registration data (RDAP) changes slowly: cache it. The RDAP limit is shared with every client of the upstream registries.

## Authentication

Keys start with `trm_live_`, are shown once at issue time, and are stored hashed. Check one with:

```bash
curl https://api.toyroom.ai/v1/ping \
  -H "Authorization: Bearer $TOYROOM_KEY"
# {"ok": true, "key": "your-name"}
```

The origin sends no CORS headers: keys are for servers and agents, not browsers.

## Credits and limits

Each key has a daily credit quota (resets 00:00 UTC, shared by REST and MCP) and each endpoint a per-minute request limit. Nothing is charged for 4xx or 502 responses.

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

## Working at scale

Synchronous endpoints answer inside the request; batch at the batch max, send at the rate limit, sleep on Retry-After. For bigger lists submit a job (next section). Ceilings per key for 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 |

Multiply list size by credits per domain and compare with `daily_quota` from /v1/usage before starting.

### Jobs: submit once, get a callback, download a file

For lists too large for one request, POST /v1/jobs takes up to 1,000,000 names (JSON, or text/plain one per line, gzip allowed, 32 MB) for type zone_lookup, appraise, or categorize. Toyroom batches and retries; rows land in one results file in input order. Credits are reserved at submit and refunded for rows never processed. Poll GET /v1/jobs/{id} (Retry-After: 30 while running) or give callback_url for a signed POST on the final state. Download GET /v1/jobs/{id}/results (NDJSON; ?format=csv; ?from=N to resume). Files are kept 24 hours. Per key: 5,000,000 names in flight across any number of jobs. A job the lane cannot start within 2 hours is refused with estimated_wait_seconds and nothing charged; a job still queued at its deadline expires with a full refund.

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

Callback signature: header Toyroom-Signature: t=<unix>,v1=<hex>, where v1 = HMAC-SHA256(webhook_secret, t + "." + body). Header Toyroom-Event: job.<status>. Six attempts over four hours; HTTPS only.

```bash
curl https://api.toyroom.ai/v1/jobs?type=zone_lookup \
  -H "Authorization: Bearer $TOYROOM_KEY" \
  -H "Content-Type: text/plain" --data-binary @names.txt
# 202 {"id": "job_…", "status": "queued", "poll_url": "/v1/jobs/job_…", …}
```

### Run a million names through the Zone Counter

Submit names.txt as a zone_lookup job (above); about 17 minutes from an idle lane, 100,000 credits. To hold the loop yourself instead, use POST /v1/zone/lookup (5,000 names per request), not /v1/zone/count (one keyword per request, for keyword research).

1. Quota: 1 credit per 10 names, so 1,000,000 names = 100,000 credits (200,000 with include_counts). If daily_quota on /v1/usage is smaller, ask feedback@toyroom.ai for a raise before starting.
2. Batches of 5,000 at 12 requests/min = 60,000 names/min. A million names takes about 17 minutes from one process.
3. Results come back in input order; write them straight to a file. On 502 nothing was charged: retry the same batch. On 429 wait Retry-After seconds.
4. include_counts (containing/starting/ending counts) is optional, slower on short words, and doubles the cost.

Every row is counts and booleans (registered or not, how many extensions, how many domains contain the name). Domain lists are never returned.

```python
import csv, os, time, requests
H = {"Authorization": f"Bearer {os.environ['TOYROOM_KEY']}"}
names = [d.strip() for d in open("names.txt") if d.strip()]
with open("zone.csv", "w", newline="") as f:
    out = csv.writer(f)
    for i in range(0, len(names), 5000):
        chunk = names[i:i + 5000]
        while True:
            r = requests.post("https://api.toyroom.ai/v1/zone/lookup", headers=H, json={"domains": chunk}, timeout=120)
            if r.status_code in (429, 502):
                time.sleep(int(r.headers.get("Retry-After", "5")))
                continue
            r.raise_for_status()
            break
        for row in r.json()["results"]:
            out.writerow([row["input"], row["registered"], row["extensions_registered"]])
        time.sleep(5)  # 12 requests/min
```

## Endpoints

## Appraisal

### POST /v1/appraise

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

- Cost: 1 credit
- Rate limit: 30 requests/min

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | required | One domain, e.g. cloudkitchen.com. |

Example:

```bash
curl https://api.toyroom.ai/v1/appraise \
  -H "Authorization: Bearer $TOYROOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "cloudkitchen.com"}'
```

Response (200):

```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
  }
}
```

| 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. |
| `zone.exact_count` | integer | How many extensions this exact name is registered under, across the zone index. |

### POST /v1/appraise/bulk

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

- Cost: 1 credit per domain
- Rate limit: 6 requests/min
- Batch max: 500 domains

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | required | Up to 500 domains. |

Example:

```bash
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):

```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"
      }
    }
  ]
}
```

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

## Classification

### POST /v1/categorize

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

- Cost: 2 credits per domain
- Rate limit: 12 requests/min
- Batch max: 100 domains

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | required | Up to 100 domains. |

Example:

```bash
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):

```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
      }
    ]
  }
}
```

| 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. |
| `summary` | object | Counts by vertical, subcategory, and type for the whole batch. |

### POST /v1/camelcase

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

- Cost: 1 credit
- Rate limit: 60 requests/min
- Batch max: 1,000 domains

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | required | Up to 1,000 domains. |

Example:

```bash
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):

```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
    }
  ]
}
```

| Field | Type | Description |
|---|---|---|
| `results[].converted` | string | CamelCase form of the domain. |
| `results[].split` | string | Space-separated words. |
| `results[].confidence` | string | HIGH, MEDIUM, or LOW. |
| `word_stats / tld_stats` | object[] | Batch-level frequency tables. |

## Zone data

### POST /v1/zone/count

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

- Cost: 2 credits
- Rate limit: 20 requests/min
- Extensions per query: up to 50

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `term` | string | required | Keyword, 2 to 63 characters, letters, digits, and hyphens. |
| `tlds` | string[] | optional | Restrict the breakdown to these extensions, up to 50. Omit for all. |
| `position` | string | optional | contains (default), starts, or ends. |

Example:

```bash
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):

```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
}
```

| 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. |
| `tld_breakdown[]` | object[] | Count per extension, largest first. |
| `total_domains_in_db` | integer | Size of the index the counts were taken from. |

Counts only. Zone endpoints never return domain names (ICANN zone-file access terms). Every number is safe to publish.

### POST /v1/zone/lookup

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

- Cost: 1 credit per 10 names
- Rate limit: 12 requests/min
- Batch max: 5,000 domains

Request body (JSON):

| Field | Type | Required | 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:

```bash
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):

```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"
}
```

| Field | Type | Description |
|---|---|---|
| `results[].registered` | boolean or null | True when the exact domain is in the index. Null when the input had no extension. |
| `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. |

### GET /v1/zone/stats

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

- Cost: 1 credit
- Rate limit: 20 requests/min

Example:

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

Response (200):

```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
    }
  ]
}
```

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

## Lookups

### POST /v1/rdap

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

- Cost: 2 credits per domain
- Rate limit: 6 requests/min
- Batch max: 10 domains

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | required | Up to 10 domains. |

Example:

```bash
curl https://api.toyroom.ai/v1/rdap \
  -H "Authorization: Bearer $TOYROOM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["google.com"]}'
```

Response (200):

```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
    }
  ]
}
```

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

Registries cap RDAP traffic globally. Cache results; prefer /v1/dns or zone counts when you only need existence.

### POST /v1/dns

**DNS records.** Resolves against public resolvers. Choose record types with record_types; the default set is A, AAAA, MX, TXT, NS.

- Cost: 1 credit per domain
- Rate limit: 12 requests/min
- Batch max: 50 domains

Request body (JSON):

| Field | Type | Required | Description |
|---|---|---|---|
| `domains` | string[] | required | Up to 50 domains. |
| `record_types` | string[] | optional | Any of A, AAAA, MX, TXT, NS, CNAME, SOA. |

Example:

```bash
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):

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

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

## Jobs

### POST /v1/jobs

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

- Cost: reserved at submit
- Rate limit: 12 requests/min

Request body (JSON):

| Field | Type | Required | 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:

```bash
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):

```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
  }
}
```

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

### GET /v1/jobs/{job_id}

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

- Cost: free
- Rate limit: not rate limited

Example:

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

Response (200):

```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
}
```

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

### GET /v1/jobs/{job_id}/results

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

- Cost: free
- Rate limit: not rate limited

Example:

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

Response (200):

```json
{
  "input": "cloudkitchen.com",
  "name": "cloudkitchen",
  "extension": "com",
  "registered": true,
  "extensions_registered": 72,
  "supported": true
}
```

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

### GET /v1/jobs

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

- Cost: free
- Rate limit: not rate limited

Example:

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

Response (200):

```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
}
```

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

### DELETE /v1/jobs/{job_id}

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

- Cost: free
- Rate limit: not rate limited

Example:

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

Response (200):

```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"
}
```

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

## Account

### GET /v1/usage

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

- Cost: free
- Rate limit: not rate limited

Example:

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

Response (200):

```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
  }
}
```

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

### GET /v1/ping

**Check a key.** Free to call and never rate limited. Use it in health checks.

- Cost: free
- Rate limit: not rate limited

Example:

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

Response (200):

```json
{
  "ok": true,
  "key": "your-name"
}
```

| 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. 429s carry `Retry-After` (seconds).

| Status | Meaning | What to do | Example 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 | Daily quota | Today'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"}` |
| 502 | Upstream unavailable | The tool behind the endpoint did not answer. Safe to retry with backoff; nothing was charged. | `{"error": "Appraisal service unavailable"}` |

## MCP server

Streamable HTTP at https://api.toyroom.ai/mcp, stateless, same key in the Authorization header, same quota as REST.

```bash
claude mcp add toyroom --transport http https://api.toyroom.ai/mcp \
  --header "Authorization: Bearer trm_live_your_key_here"
```

Generic client config:

```json
{
  "mcpServers": {
    "toyroom": {
      "type": "http",
      "url": "https://api.toyroom.ai/mcp",
      "headers": {
        "Authorization": "Bearer trm_live_your_key_here"
      }
    }
  }
}
```

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