Revised

Keep a local copy in sync

Poll GET /api/v1/domains with since to receive only what changed, including tombstones for listings that have left.

If you hold a local copy of the directory, since is how you keep it current without re-paging 17,713 rows. A since page returns listings whose updated_at is at or after the instant you pass, plus tombstones for listings that have left.

The watermark rule

Every page carries synced_at. Pass the first page’s value as the next run’s since. Not the last page’s, and not your own clock.

Two properties make that correct:

  • synced_at is read before any row is, so a listing updated halfway through a long run is picked up on the next run rather than falling into the gap.
  • It trails server time by 120 seconds, deliberately. A row written just before your poll cannot fall through the window between being stamped and being committed.

Consecutive polls therefore overlap by design. You will see rows you already have. That is the feed working, not a bug.

Every row is idempotent

Every row is idempotent by id plus updated_at. Because of that overlap, the only safe consumer is an upsert keyed on id, which ignores a row it already holds at the same updated_at.

python
import time

import requests

BASE = "https://getrevised.com"


def sync(session, since=None, limit=250, pause=0.6):
    """Pull everything changed since `since`. Returns (upserts, deletes, watermark)."""
    upserts, deletes = [], []
    cursor, watermark = None, None

    while True:
        params = {"limit": limit}
        if since:
            params["since"] = since
        if cursor:
            params["cursor"] = cursor

        response = session.get(f"{BASE}/api/v1/domains", params=params, timeout=30)
        if response.status_code == 402:
            raise RuntimeError("the since feed is Business-only; check sync_feed on /me")
        response.raise_for_status()
        payload = response.json()

        if watermark is None:
            watermark = payload["synced_at"]  # FIRST page only

        for row in payload["data"]:
            if row["status"] == "delisted":
                deletes.append(row["id"])
            else:
                upserts.append(row)

        if not payload["has_more"]:
            return upserts, deletes, watermark
        cursor = payload["next_cursor"]
        time.sleep(pause)
python
upserts, deletes, watermark = sync(session, since="2026-09-21T03:12:07.000Z")
print(len(upserts), "changed,", len(deletes), "delisted; next since =", watermark)
Output
text
612 changed, 27 delisted; next since = 2026-09-22T03:12:07.000Z

Advance your stored watermark only when the whole run completed. If a page fails halfway through, discard the new watermark and re-run from the old one — the overlap costs you a few duplicate rows, and skipping forward costs you rows you will never see again.

Tombstones

A row that has left the directory comes back with nothing but three fields:

json
{
  "id": "X00w46xl",
  "status": "delisted",
  "updated_at": "2026-09-21T14:22:08.000Z"
}

It is reported the same way whether the listing was delisted, sold or blocked — the reason is not disclosed. Every row in a since page carries status, so read that before you treat a row as a listing. A consumer that assumes data[] is homogeneous will try to read tier off a tombstone and fail.

Outside the change feed, GET /api/v1/domains/{id} simply returns 404 once a listing has gone.

First run

There is no bootstrap endpoint. Page the whole directory once without since, keep the first page’s synced_at, and poll from there.

python
rows, _, watermark = sync(session)          # full crawl, no since
store_all(rows)
save_watermark(watermark)

# later, on a schedule
changed, gone, watermark = sync(session, since=load_watermark())

Pick a poll interval that fits your rate limit: 120 on Business, per key, per minute.

What the feed does not tell you

Hold state does not move updated_at. Taking, extending or ending a hold is invisible to since, so a row whose hold_state flipped will not be re-delivered. If hold state matters to you, read the listing directly, or call GET /api/v1/holds for your own holds.

sort=latest cannot be combined with since. The change feed is ordered by listing id, and its cursors belong to that ordering. Use sort=latest for “what is new in the directory” browsing, and since for synchronisation — they are different jobs.

discovered_at is not updated_at. discovered_at is written once, when the listing entered the directory, and never rewritten — which is what makes sort=latest safe to page. It is a fact about the directory, not about the domain: it is neither the registration date nor the date the name expired.

Storing what comes back

Two details will bite a schema that ignores them:

  • Ids are case-sensitive. 00aLaXEG and 00alaxeg are not the same listing. Use a case-sensitive column and a case-sensitive unique index.
  • A null metric means absent, not zero. Keep metrics_missing alongside the row, or store the metrics as nullable and never coalesce them to 0 on the way in. See Concepts.

Type to search…

↑↓ navigate openesc close