Sync your book
Keep a copy of your BrokerPlus records in your own database or warehouse. This guide walks through
the code: the first full copy, the incremental sync after it, and the records that drop out of a
list. For when to use the change feed, updated_since, or webhooks, read Sync first.
Give the sync its own key with the read_only limit. A sync never needs to write. See
Authentication.
Sync with the change feed
The change feed is the better path. It never skips a change, and it tells you when a record is
deleted, merged, or archived. GET /v1/changes covers contacts, leads, deals, tasks, intakes,
documents, appointments, and communications.
- Call
GET /v1/changes?after=nowand save thenext_checkpoint. - Copy every record from the lists, for example
GET /v1/contacts, page by page. - Call
GET /v1/changes?after=<checkpoint>, apply each entry, and save the newnext_checkpoint. - While
has_moreistrue, repeat step 3 right away. When it isfalse, wait a while, then repeat.
Take the checkpoint in step 1 before the copy in step 2. Anything that changes during the copy is then waiting for you in the feed.
To apply an entry, read the record again by record_id and upsert it. Three actions need a little
more:
deleted: mark the record as removed in your copy.archived: read the record again. It left the active book but you can still read it, and an archived lead stays in the lead list.merged: point your references atmerged_into_id, then remove the record.
Code
Things that trip people up:
- A checkpoint belongs to the
typesyou sent with it. Keep one checkpoint pertypesvalue. Sending it with more types returns400 INVALID_CHECKPOINT. - A checkpoint older than 30 days returns
410 CHECKPOINT_EXPIRED. Start again from step 1. - A page can hold fewer entries than
limitand still havehas_more: true. Decide onhas_more, never on the count. - Entries arrive in the order the changes became visible, which can differ from
occurred_at. - The feed covers the record's own fields. Addresses, notes, the mortgage application, and tags aren't in it yet, so read those from their own endpoints on a schedule.
Sync with updated_since
Use this loop for a list the change feed doesn't cover, or for a one-off copy. Most record lists
take updated_since and after_id; the API reference shows which. Small lists such
as users, teams, pipelines, and lenders don't need it: read them in full each run.
With updated_since, rows come back oldest updated_at first, with id breaking ties. So the
last row you saved is your position in the list. Send it back to get what comes next:
| Parameter | Value |
|---|---|
updated_since | The updated_at of the last row you saved. |
after_id | The id of the last row you saved. |
Keep page at 1 and stop when data is empty. For each list:
- Load your saved cursor (an
updated_atand anid). On the first run there is none: sendupdated_since=1970-01-01T00:00:00Zand noafter_id. - Request the list with
page_size=100. - Upsert each row into your database by
id. - Save the last row's
updated_atandidas the new cursor. - If
datawasn't empty, go back to step 2.
Save the cursor after you write the rows. If the job dies in between, the next run reads a few rows twice, which an upsert makes harmless. A cursor saved too early skips rows.
Don't sync with page=2, page=3, and so on. A row that changes mid-sync jumps to the end of the
list, every row behind it shifts up one place, and you miss one. The cursor doesn't move when other
rows change. Sync has the details.
Catch records that leave a list
A list quietly drops a record that was deleted, archived, or merged, and the updated_since loop
never sees it again. Use the change feed where it covers the record. Otherwise, run a reconcile job
once a day or once a week:
- Read the full list, with no
updated_since, and collect everyid. - Compare that set with the ids in your copy.
- For each id only you have, call
GET /v1/{list}/{id}. A404means the record is gone from your key's reach, so mark it removed.
A 404 doesn't always mean deleted. A record that moved to a team outside your key's reach also
returns 404. And a merged or archived contact still answers GET /v1/contacts/{id}, so trust the
list, not the single read, to decide whether a contact is still in the book. Sync lists
which records leave which list.
Stay inside the limits
- Send
updated_sinceexactly as the API returnedupdated_at, for example2026-10-01T14:03:22.118Z. Anything that isn't an ISO 8601 date-time, orafter_idwithoutupdated_since, gets400 VALIDATION_ERROR. - At 100 rows a request, the 600-requests-a-minute limit reads 60,000 rows a minute. See Authentication.
- Run one sync at a time per key. Two jobs on one key share one rate limit.
Get told instead of polling
A webhook tells you something changed, so you don't have to poll. Treat it as a nudge to read the change feed from your checkpoint, not as the copy itself: a webhook can arrive late, twice, or not at all. See Webhooks.