Pagination and sync
Opaque cursor paging on GET /api/v1/domains, the synced_at watermark, the since change feed, and tombstones.
GET /api/v1/domains is the only paged endpoint. It pages by opaque cursor, and on the Business plan the same endpoint doubles as a change feed.
The page envelope
{
"data": [ /* up to `limit` listings, or tombstones on a `since` page */ ],
"next_cursor": "00aLaXEG",
"has_more": true,
"synced_at": "2026-09-22T01:45:55.000Z"
}| Field | Type | What it is |
|---|---|---|
data | array | Listings. On a since page it mixes listings and tombstones — read status on every row. |
next_cursor | string or null | Pass as cursor for the next page. null on the last page. |
has_more | boolean | Mirrors next_cursor !== null. |
synced_at | string | The watermark for your next run. See Incremental sync. |
limit
| Default | Maximum | Minimum |
|---|---|---|
| 50 | 250 | 1 |
An integer out of range is clamped, not rejected — ask for 5000 and you get 250, ask for 0 and you get 1.
A limit that is not a whole number is refused with 400 invalid_request, and that includes the empty string. ?limit= is what ?limit=${n} sends for an undefined n, and answering it with a page size of 1 would quietly make a full sync 250 times longer.
cursor
cursor is an opaque string taken from the previous response’s next_cursor.
A cursor belongs to the ordering it came from, so pass the same sort on every page of a run. The default ordering’s cursor is a listing id; sort=latest’s is a position in that order.
Ordering
sort | Ordering | Cursor-safe because |
|---|---|---|
| omitted | Ascending listing id. The default since v1. | A listing id is written once and never rewritten. |
latest | Newest discovered_at first, tie-broken by id. | discovered_at is written once and never rewritten. |
No other value is accepted. Every other ordering the website offers is over a column an update can move, and a feed that reshuffles under a cursor can skip or repeat rows — so the endpoint refuses those rather than serving one.
latest cannot be combined with since: the change feed is ordered by id.
Paging the whole directory
Follow next_cursor until it comes back null. This works on every plan.
curl "https://getrevised.com/api/v1/domains?limit=250&rsMin=60" \
-H "Authorization: Bearer $REVISED_API_KEY"import os, requests
BASE = "https://getrevised.com/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['REVISED_API_KEY']}"}
def page_all(**filters):
"""Every listing matching `filters`, one page at a time."""
cursor = None
while True:
params = {"limit": 250, **filters}
if cursor:
params["cursor"] = cursor
page = requests.get(f"{BASE}/domains", headers=AUTH, params=params, timeout=30).json()
yield from page["data"]
cursor = page["next_cursor"]
if cursor is None:
return
count = sum(1 for _ in page_all(rsMin=60, tld="com"))
print(f"{count} listings")1843 listingsPaging cannot skip or repeat a row while the directory is updated underneath you, because both orderings are over write-once columns.
Incremental sync
since takes an ISO 8601 instant and is matched, inclusively, against each row’s updated_at.
curl "https://getrevised.com/api/v1/domains?since=2026-09-21T23:45:55.000Z&limit=250" \
-H "Authorization: Bearer $REVISED_API_KEY"Use synced_at, not your own clock
Pass the synced_at from the first page of your previous run as the next run’s since. Never datetime.now().
synced_at is taken before any row on that page is read, so a listing updated part-way through a run is picked up by the next run rather than skipped. It is also held deliberately 120 seconds behind server time, which is what makes a row written just before your previous poll impossible to miss: a row is stamped a moment before it is committed, and a watermark taken in that gap would have filtered it out for good.
Consecutive polls therefore overlap on purpose. Expect to see rows you already hold, and write them straight through — a row is fully described by its id plus its updated_at, so upsert on id and ignore a row you already hold at that updated_at.
Changed includes gone
A since page mixes full listings with tombstones: rows that have left the directory since your watermark.
{
"id": "00aLaXEG",
"status": "delisted",
"updated_at": "2026-09-02T04:11:08.512Z"
}A tombstone carries id, status and updated_at and nothing else — no name, because you already hold the name against that id from the poll that delivered it. Sold, blocked and delisted all report as delisted; they mean the same thing to a consumer.
Read status before treating a row as a listing. active is a listing to upsert; delisted is an id to drop. A first sync with no since returns listings only.
A complete sync loop
import os, requests
BASE = "https://getrevised.com/api/v1"
AUTH = {"Authorization": f"Bearer {os.environ['REVISED_API_KEY']}"}
def sync(since: str | None) -> str:
"""Apply everything changed since `since`. Returns the next watermark."""
cursor, watermark = None, None
while True:
params = {"limit": 250}
if since:
params["since"] = since
if cursor:
params["cursor"] = cursor
page = requests.get(f"{BASE}/domains", headers=AUTH, params=params, timeout=60).json()
# Take the watermark from the FIRST page only: it was read before
# any row was, so a row written mid-run is caught by the next run.
if watermark is None:
watermark = page["synced_at"]
for row in page["data"]:
if row["status"] == "delisted":
drop(row["id"])
else:
upsert(row) # idempotent on (id, updated_at)
cursor = page["next_cursor"]
if cursor is None:
return watermark
watermark = load_watermark() # None on a first run
watermark = sync(watermark)
save_watermark(watermark)applied 312 listings, dropped 4 ids
next watermark: 2026-09-22T01:45:55.000ZWhat since does not catch
Taking, extending or ending a hold does not move a listing’s updated_at, so an incremental poll will not re-deliver a row whose hold state changed. Read the listing directly with GET /api/v1/domains/{id}, or call GET /api/v1/holds for your own.
Listing ids
Ids are eight characters and case-sensitive, mixed case in practice (00aLaXEG, X00w46xl). Store them exactly as they arrive; a lower-cased id matches nothing, including in a cursor.
Filters and paging together
A cursor names a position in the ordering, not the query that produced it. Change a filter mid-run and the next page continues from that position under the new filters, silently skipping every matching row behind it — no error, just a short result. Fix your filters before the first page and keep them, along with sort, for every page of the run. See Filters.