URL: /docs/guides/finding-domains

---
title: Find domains
description: Filter and page the directory with GET /api/v1/domains without burning a rate-limit window.
---

`GET /api/v1/domains` is the endpoint you will spend most of your requests on. It is free and unlimited inside your rate limit — a page never charges for the rows it matched, whatever tier they are on. Filter structurally on the server, page with the cursor, then narrow locally.

This guide assumes you have a key and know what a tier is. If not, start with the [Quickstart](/docs/quickstart).

## Filter on the server first

```bash
curl -s -G https://getrevised.com/api/v1/domains \
  -H "Authorization: Bearer $REVISED_KEY" \
  -d category=education \
  -d tld=com \
  -d spam=low \
  -d rsMin=70 \
  -d limit=250
```

| Parameter | Accepts | Notes |
| --- | --- | --- |
| `category` | A category code, comma-separated for several | See [the codes](#category-codes). An unknown code is refused. |
| `tld` | An exact TLD without the dot — `com`, `com.au` | |
| `spam` | `low`, `moderate` | |
| `tier` | `open` | The only value this filter accepts. There is no way to ask for `regular` or `featured` rows specifically. |
| `featured` | boolean | Reads the shelf rank. Not the same thing as `tier == "featured"`. |
| `recommended` | boolean | Listings that pass the recommended heuristic: categorised, at least one marquee linker, and a Revised Score floor. |
| `rsMin`, `rsMax` | 0–100 | Bounds on the published Revised Score range. `rsMin` filters the lower bound. |
| `citabilityMin` | 0–100 | Minimum Agent Citability score. |
| `ageMin` | 0–100 | Minimum age in whole years. **Rows with a null `age_years` never match** — see below. |
| `checkedDays` | 1–3650 | Only listings whose availability was checked within that many days. |
| `rd` | Band **ids**, comma-separated: `lt10`, `10-50`, `50-100`, `100-250`, `250plus` | Referring-domain bands. The ids are not the values the response returns — see below. |
| `source` | Marquee-linker display names, comma-separated, case preserved — `Hacker News,Wikipedia` | |
| `priceRange` | `under-100`, `100-249`, `250-999`, `1000-2499`, `2500-plus` | Buckets the same figure the response reports as `estimated_value_cents`, so a band and the values it returns agree. |
| `q` | 1–80 characters | Free text across the masked hint, tags, category and blurb. |
| `sort` | `latest` | Omitted: ascending listing id. `latest`: newest `discovered_at` first. |
| `limit` | 1–250, default 50 | Clamped, not rejected — see below. |
| `cursor` | An opaque cursor from a previous response | |
| `since` | ISO 8601 instant | **Business plan only.** See [Incremental sync](/docs/guides/incremental-sync). |

Every other endpoint and parameter on this API answers on every plan.

<Warning>
  `q` deliberately does **not** search the real domain name — not on any plan, and not for any tier. It covers the masked hint, keyword tags, category and blurb. Searching for a name you already have in mind will not find it. Use `GET /api/v1/domains/{id}` when you know the id.
</Warning>

## Page with the cursor

Paging is by opaque cursor, not offset. Stop on `has_more`, never on an empty page, and keep `sort` the same across every page of a run — a cursor belongs to the ordering it came from.

```python
import time

import requests

BASE = "https://getrevised.com"


def fetch_listings(session, max_pages=10, limit=250, pause=0.6, **filters):
    """Page /api/v1/domains with a cursor. Returns (rows, watermark)."""
    rows, cursor, watermark = [], None, None

    for _page in range(max_pages):
        params = dict(filters, limit=limit)
        if cursor:
            params["cursor"] = cursor

        response = session.get(f"{BASE}/api/v1/domains", params=params, timeout=30)
        response.raise_for_status()
        payload = response.json()

        rows.extend(payload["data"])
        if watermark is None:
            watermark = payload["synced_at"]  # first page only — see Incremental sync

        if not payload["has_more"]:
            break
        cursor = payload["next_cursor"]
        time.sleep(pause)
    else:
        print(f"stopped at the {max_pages}-page cap — narrow the filters or raise it")

    return rows, watermark
```

Three details in that loop are load-bearing:

- **The `max_pages` cap.** A bug in a filter cannot then run away with your whole rate-limit window.
- **`time.sleep(pause)`.** Your ceiling is per key per minute: 20 on Free, 60 on Pro, 120 on Business. At `limit=250` a 0.6-second pause keeps a Free key comfortably inside its window.
- **The watermark comes from the first page only.** It is read before any row is, so a listing updated mid-run is picked up next time rather than skipped.

The same thing in shell, one page at a time:

```bash
curl -s -G https://getrevised.com/api/v1/domains \
  -H "Authorization: Bearer $REVISED_KEY" \
  -d tier=open -d limit=250 \
  | jq -r '.next_cursor, .has_more, (.data | length)'
```

```text Output
c7Kd0Pq1
true
250
```

<Warning>
  Treat the cursor as opaque and never construct one. The value is checked for shape **and** for a position the directory actually holds, so a cursor this endpoint did not issue is a `400 invalid_request` — never an empty page, and never a silent resume from the middle of the feed.
</Warning>

## Category codes

Category is the strongest filter the directory has, and the codes are Revised's own — `marketing` and `software` are not among them. There is no categories endpoint on the REST API, so hardcode the list. (The [MCP server](/docs/mcp/introduction) does expose a free `list_categories` tool.)

Counts are as of 22 September 2026.

| Code | Label | Listings | On the open shelf |
| --- | --- | --- | --- |
| `other` | Other | 1,553 | 359 |
| `media` | Media & Entertainment | 1,154 | 260 |
| `saas` | SaaS | 599 | 155 |
| `education` | Education | 512 | 114 |
| `ecommerce` | E-Commerce | 440 | 86 |
| `healthcare` | Healthcare | 186 | 33 |
| `travel` | Travel & Hospitality | 174 | 39 |
| `social` | Social Media | 168 | 38 |
| `gaming` | Gaming | 133 | 29 |
| `productivity` | Productivity | 112 | 22 |
| `real-estate` | Real Estate | 108 | 15 |
| `fintech` | Fintech | 106 | 20 |
| `food` | Food & Beverage | 104 | 22 |
| `automotive` | Automotive | 90 | 16 |
| `crypto` | Crypto & Web3 | 84 | 14 |
| `ai` | AI & Machine Learning | 79 | 26 |
| `security` | Security | 79 | 18 |
| `fitness` | Fitness & Wellness | 75 | 15 |
| `legal` | Legal | 52 | 15 |
| `iot` | IoT & Hardware | 29 | 5 |

Those counts total 5,837 categorised listings out of 17,713 in the directory, so **a category filter reaches about a third of it**. The rest carry no category at all and no `category` value will return them. If breadth matters more than precision, filter on something else and sort locally.

An unknown code is refused with `400 invalid_request` rather than answered with an empty page, which is what you want — a typo fails loudly instead of looking like a quiet day in the directory.

## Traps

**`limit` is clamped, but an empty `limit` is refused.** Ask for 5,000 and you get 250; ask for 0 and you get 1. But `?limit=` — which is what `?limit=${n}` sends for an undefined `n` — is a `400`, on purpose: answering it with a page size of 1 would quietly make a full sync 250 times longer.

**Minimum filters exclude null rows.** `rsMin`, `citabilityMin` and `ageMin` drop rows where the metric is missing rather than ranking them low. Since `agent_citability` is `null` whenever the domain has no row in the citability index, a mild-looking `citabilityMin=20` can remove most of a page. Check `metrics_missing` on the rows you do get back.

`ageMin` is the one that catches people out, because age is unknown on a large share of the directory — 9 of the 30 open-tier rows bundled with the [Python walkthrough](/docs/examples/python-walkthrough) carry a null `age_years`. So `ageMin=1` does not mean "at least a year old"; it means **"at least a year old, and we know how old it is"**. If you want old domains without silently dropping every unaged row, filter on something else and sort on `age_years` locally, treating `null` as unknown rather than young.

**`rd` takes band ids, not band values.** The response reports `referring_domains` as `"<10"`, `"10-50"`, `"50-100"`, `"100-250"` or `"250+"`. The filter takes `lt10`, `10-50`, `50-100`, `100-250` or `250plus` — the ids exist so you never have to percent-encode `<` or `+`. Only the middle three coincide, so copying a value straight out of a response into the filter is a `400` on exactly the two ends of the range you are most likely to want.

```bash
# WRONG — these are response values, not filter ids
curl -s -G https://getrevised.com/api/v1/domains -d 'rd=<10,250+' ...

# RIGHT
curl -s -G https://getrevised.com/api/v1/domains \
  -H "Authorization: Bearer $REVISED_KEY" \
  -d rd=100-250,250plus \
  -d source='Hacker News,Wikipedia'
```

**`sort=latest` cannot be combined with `since`.** The change feed is ordered by id.

**Unpublished parameters are ignored, published ones are enforced.** A parameter this API documents, carrying a value it cannot honour, is a `400` naming the parameter and what it accepts — never a silently wider page. Parameters it does not document are ignored, so tracking and cache-busting keys on the query string are harmless.

**Read `status` before you read anything else.** A `since` page mixes full listings with tombstones. Outside the change feed every row is `status: "active"`, but branching on it costs nothing and makes the code correct if you later add `since`.

## Errors

| Status | `error.code` | What happened |
| --- | --- | --- |
| 400 | `invalid_request` | A published parameter carried a value it cannot honour, or a cursor this endpoint did not issue. The message names the parameter. |
| 401 | `unauthorized` | No key, or a key that no longer resolves. |
| 402 | `upgrade_required` | On this endpoint it means exactly one thing: `since` on a plan without the change feed. |
| 405 | `method_not_allowed` | Carries `Allow`. |
| 429 | `rate_limited` | Carries `Retry-After` alongside the `X-RateLimit-*` headers every response has. |

## Next

<CardGroup cols={2}>
  <Card title="Evaluate a domain" icon="scale" href="/docs/guides/evaluating-a-domain">
    Read a row properly: bands, missing data, linkers, the availability stamp.
  </Card>
  <Card title="Reveals and holds" icon="lock" href="/docs/guides/revealing-and-holds">
    Turn a mask into a name, then reserve the listing.
  </Card>
</CardGroup>
