# Staying in sync

There are three ways to find out what changed in a brokerage. They work best together: webhooks
tell you **when** to look, the change feed tells you **what** changed, and `updated_since` covers the
lists the feed does not.

| Tool                    | What it gives you                                   | Can it miss a change?                  |
| ----------------------- | --------------------------------------------------- | -------------------------------------- |
| `GET /v1/changes`       | Every change, in order, with deletes and merges     | No, within 30 days                     |
| Webhooks                | A signed POST to your server, seconds after a change | Yes. Treat it as a hint                |
| `updated_since`         | The rows of one list that changed since a time      | No, but it cannot see deletes          |

For a working sync loop with code, follow the [Sync your book](/sync-your-book) guide.

## The change feed

`GET /v1/changes` is the source of truth for a sync. It returns every change to the contacts,
leads, deals, tasks, intakes, documents, appointments, and communications your key can read, in
the order the changes took effect. BrokerPlus writes each entry in the same database transaction as
the change itself, whether the change came from the app, an import, an automation, or the API. A
checkpoint never skips an entry.

```bash
curl "https://api.mos.getbrokerplus.ca/v1/changes?after=MS4xMjAwLjE3OTE...&types=contact,deal" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

```json
{
  "success": true,
  "data": [
    {
      "object": "change",
      "id": "8812",
      "record_type": "deal",
      "record_id": "cmsdeal4k...",
      "action": "updated",
      "changed_fields": ["stage_id", "stage_entered_at"],
      "merged_into_id": null,
      "occurred_at": "2026-10-07T14:20:00.000Z"
    }
  ],
  "pagination": { "next_checkpoint": "MS4xMjE0LjE3OTE...", "has_more": false }
}
```

What each `action` asks you to do:

| `action`              | Do this                                                                |
| --------------------- | ---------------------------------------------------------------------- |
| `created`, `updated`  | Read the record again by `record_id`, and upsert it.                   |
| `archived`            | Read it again. It left the active book, but you can still read it.     |
| `deleted`             | Remove it from your copy.                                              |
| `merged`              | Point your references at `merged_into_id`, then remove it.             |

The rules that matter:

- **Start with `after=now`.** It returns a checkpoint and no entries. Take it before you copy the
  lists, so a change made during your copy is still in the feed.
- **Save `next_checkpoint` after you apply the page**, not before.
- **Follow `has_more`, not the page size.** A page can be short, or even empty, while `has_more` is
  `true`. Call again at once. When it is `false`, wait and poll again.
- **A checkpoint belongs to its `types`.** Keep one checkpoint per `types` value you use. You can
  read it with the same `types` or fewer. Reading more types from it would skip their entries, so
  that returns `400 INVALID_CHECKPOINT`. A checkpoint from `after=now` belongs to the `types` you
  sent with it, or to every type when you sent none.
- **Entries are kept for 30 days.** An older checkpoint returns `410 CHECKPOINT_EXPIRED`. Take a new
  one with `after=now`, copy the lists again, and carry on from there.
- The feed covers the fields of the record itself. Rows that hang off a record, such as addresses,
  notes, or the mortgage application, are not in it. Read them when their parent changes.

## Webhooks

A webhook is a signed `POST` that BrokerPlus sends to your server when something happens, such as
`lead.created` or `deal.stage_changed`. It arrives in seconds, so it is the fastest signal you get.

A webhook is a **hint**, not a record of truth. A delivery can arrive late, arrive twice, or, after
an outage longer than its retry window, not arrive at all. So on each delivery, read the change feed
from your checkpoint, or read the record by its id. Never build your copy from the webhook bodies
alone.

How to register an endpoint, verify a signature, and read the delivery log is on the
[Webhooks](/webhooks) page.

## `updated_since` on a list

The main record lists accept `updated_since`: contacts, leads, deals, tasks, intakes, appointments,
submissions, communications, opportunities, and campaigns. The [API reference](/reference) shows it
on each list that has it.

With `updated_since`, a list returns the rows that changed at or after that time, oldest change
first, with ties broken by `id`. To read on from the last row you saved, send its `updated_at` as
`updated_since` and its `id` as `after_id`, and keep `page` at `1`:

```bash
curl "https://api.mos.getbrokerplus.ca/v1/contacts?updated_since=2026-10-01T14:03:22.118Z&after_id=cmcontact8f2...&page_size=100" \
  -H "Authorization: Bearer $BROKERPLUS_API_KEY"
```

Don't sync with page numbers. A row that changes while you read moves to the end of the order, every
later row shifts up one place, and the next page skips a row. The `updated_at` and `id` pair does
not move, so nothing is skipped.

A value of `updated_since` that is not a date returns `400 VALIDATION_ERROR`, and so does `after_id`
without `updated_since`.

### Records that leave a list

A list cannot tell you that a record is gone. It just stops returning it.

| Record  | Leaves the list when                                                 |
| ------- | -------------------------------------------------------------------- |
| Contact | It is deleted, archived, or merged into another contact.             |
| Deal    | It is deleted, or BrokerPlus marks it as a test.                     |
| Lead    | It is deleted. An archived lead stays, with `archived_at` set.       |

The change feed reports all of these. For a list the feed does not cover, run a reconcile now and
then: read the full list, and remove from your copy each id that is no longer in it.

## Small lists

Users, teams, pipelines, tags, lenders, and the other setup lists are small and change rarely. Read
them in full on each run.
