# API reference

domainsearch answers one question, *is this domain registered?*, over a
plain JSON API. No account, no API key, no cookies. Base URL:

```
https://domainsearch24.eu/api/v1
```

<TableOfContents depth="2" />

## How availability is determined

Every check runs through layered signals, ordered by cost and freshness,
and **never guesses**. The availability status is one of:

| Status | Meaning |
|---|---|
| `registered` | The domain is taken. |
| `free` | The registry itself answered "not registered" (RDAP 404). |
| `unknown` | No signal was available: see the `reason` field. |

Each answer names its `source`:

| Source | Layer | Freshness |
|---|---|---|
| `czds` | ICANN zone files: every delegated name per covered gTLD | ≤ 24 h |
| `commoncrawl` | Common Crawl web-graph domain set (~120 M domains) | months |
| `dns` | NS delegation probe: works for every TLD | live |
| `rdap` | The registry's own RDAP answer: the only source that can assert `free` | live |

Two important consequences:

- **`free` is only assertable where RDAP exists.** Some ccTLDs (e.g. `.eu`)
  run no public RDAP; their unregistered domains stay honestly `unknown`.
  Use [`/tlds`](#get-tlds) to know upfront.
- **`free` is a moment-in-time registry answer**, not a reservation.

## GET /check

```
GET /api/v1/check?domains=example.com,example.io[&fast=1]
```

Checks up to **30** comma-separated domains and returns all results in one
response (extra domains beyond 30 are dropped). Domains must be registrable
names in punycode (IDNs are not auto-encoded). Whole-batch budget: ~9 s;
slow registries yield per-domain `unknown` and retries are cheap (availability results
are cached server-side: registered 24 h, free 1 h, unknown 10 min).

`fast=1` skips RDAP and answers from the local layers only: milliseconds,
but it can assert `registered`, never `free`. Use it to pre-filter
candidate lists before confirming the survivors with a full check (or use
[streaming](#get-checkstream) and get both in one request).

```bash
curl 'https://domainsearch24.eu/api/v1/check?domains=example.com,zqhx.dev'
```

```json
{
  "results": [
    {"domain": "example.com", "status": "registered", "source": "czds"},
    {"domain": "zqhx.dev", "status": "free", "source": "rdap"}
  ]
}
```

## GET /check/{domain}

The curl-friendly single-domain form. Same semantics, bare result object.

```bash
curl https://domainsearch24.eu/api/v1/check/example.com
```

```json
{"domain": "example.com", "status": "registered", "source": "czds"}
```

## GET /check/stream

```
GET /api/v1/check/stream?domains=example.com,example.io[&fast=1]
```

Same input as `/check`, but the response is **NDJSON**
(`application/x-ndjson`): one result object per line, written as each
domain resolves. Local-layer answers arrive in milliseconds while RDAP
confirmations trail in over a few seconds: ideal for progressive UIs.
Line order is completion order, not input order; match on `domain`. The
stream ends when every domain has answered (budget ~15 s).

```bash
curl -N 'https://domainsearch24.eu/api/v1/check/stream?domains=a.com,b.io,c.dev'
```

## GET /bulk/stream

```
GET /api/v1/bulk/stream?label=acme[&confirm=1][&tlds=com,io,dev]
```

The bulk search behind the landing page: ONE label checked across **every
delegated TLD** (~1,450 domains) on a single NDJSON stream.

By default only the fast local layers run (zone files, Common Crawl,
DNS): seconds for the whole TLD space, no registry traffic. A **clean local
miss classes as `free`**: no zone entry, no crawl entry, NXDOMAIN. Its
strict `status` stays `unknown`: only a registry can formally assert
free: and the registrar checkout is the final word either way.

`tlds=com,io,dev` restricts the sweep to a comma-separated subset
(unknown TLDs are ignored). DNS probe outcomes are cached server-side for
24 hours, so repeated sweeps are effectively instant.

`confirm=1` adds a second phase: RDAP for the still-unknown names on
RDAP-capable TLDs, popular TLDs first, until the ~90 s budget runs out.
Upgrade lines stream with the same `domain`; a `confirm` line supersedes
the `local` one (upgrading assumed-free to registry-confirmed `free`, or
catching the rare registered-but-undelegated name).

Each line adds a `class` refining the raw status for filtering:

| class | Meaning |
|---|---|
| `taken` | Registered. |
| `free` | No registration found (clean local miss), or registry-confirmed when `status` is `free`. |
| `premium` | Registered with registry status `reserved`: typically premium/registry-held inventory. |
| `aftermarket` | Registered, but delegated to a known parking/for-sale marketplace nameserver (heuristic). |
| `unknown` | No signal either way: e.g. the DNS probe failed, or confirmation is pending. |

The stream opens with `{"label":"acme","total":1438}` and closes with
`{"done":true,"confirm":…,"queued":…,"confirmed":…,"truncated":…}`:
`truncated: true` means the confirmation budget ran out before every
queued name was verified.

```bash
curl -N 'https://domainsearch24.eu/api/v1/bulk/stream?label=acme'
```

Bulk searches are rate limited per IP as whole searches (they already fan
out server-side); API keys lift the limit.

## GET /tlds

Every delegated root-zone TLD (per IANA) with capability flags:

```json
{"tlds": [{"tld": "com", "rdap": true, "zone": true}, ...]}
```

- `rdap`: an RDAP authority is known, so `free` is assertable.
- `zone`: a zone-file index is loaded, so `registered` answers are
  complete to within ~24 h without any registry lookup.

Cacheable for an hour; the underlying sources change at most daily.

## GET /status

Layer readiness and uptime: also our health check:

```json
{
  "ok": true,
  "uptime_s": 86400,
  "layers": {
    "dns": {"enabled": true, "ready": true},
    "rdap": {"enabled": true, "ready": true},
    "czds": {"enabled": true, "ready": true, "detail": "853 zones indexed"},
    "commoncrawl": {"enabled": true, "ready": true}
  }
}
```

## Errors

Errors use one envelope, always JSON:

```json
{"error": {"code": "rate_limited", "message": "anonymous rate limit exceeded; retry later or use an API key"}}
```

| HTTP | code | When |
|---|---|---|
| 400 | `invalid_request` | No domains given. |
| 404 | `not_found` | Unknown endpoint. |
| 429 | `rate_limited` | Anonymous allowance exhausted: honor `Retry-After`. |
| 501 | `unavailable` | Checking is not configured on this deployment. |

Malformed domains are **not** request errors: they come back as per-domain
`unknown` results with a `reason`, so one bad name never fails a batch.

## Rate limits & keys

Anonymous use is limited **per IP, per domain checked** (a 30-domain batch
costs 30 units): 120 domains/minute with burst headroom of twice that.
That is plenty for interactive use.

Calling the API from a service? An API key lifts the limit: request one
at [liam@domainsearch24.eu](mailto:liam@domainsearch24.eu) and send it as
`Authorization: Bearer <key>`.

## CORS & OpenAPI

The API sends `Access-Control-Allow-Origin: *`: call it straight from the
browser. A machine-readable spec is at
[/api/openapi.yaml](/api/openapi.yaml); this page is also served as plain
markdown at [/docs.md](/docs.md) for LLM agents (see [/llms.txt](/llms.txt)).
