# Preview a change to your CRM

Source: https://help.ruuster.com/api/crm-change-preview

```http
POST https://api.ruuster.com/v1/crm/changes
```

Step one of changing your CRM. Says exactly what a change would do (add or remove tags, change stage, reassign, add a note or a task) to each person, with warnings, and records the request. Writes NOTHING to your CRM; nothing changes until you confirm the preview with Apply a change.

- Authentication: `x-api-key` and `x-email` headers of an owner or admin
- Rate limit: *lookup* call, 20 per minute per key, within the 120 per minute
- Changes your CRM only after a preview and an explicit confirmation; needs the `x-crm-write: allow` header. Every change is kept on record
- Your CRM: `x-crm-vendor` and `x-crm-key` headers with your CRM's API key, used for this request only. See [Connect your CRM](https://help.ruuster.com/api/crm)
- Rows count against the daily lead-row quota (50,000 by default)

Changing your CRM through Ruuster always takes two calls. This one reads the people from your CRM, works out each person's before and after, skips anyone already in the right state, and lists what could matter (automations that run on tags or stages, notifications to agents). It records the request and the preview, and writes nothing to the CRM. To make the change, the same person confirms it with [Apply a change](https://help.ruuster.com/api/crm-change-apply), no sooner than 8 seconds and no later than 15 minutes after the preview. Every change, every person it touched and every call against it stays on record: [List CRM changes](https://help.ruuster.com/api/crm-changes-list).

Needs your CRM key (`x-crm-vendor`, `x-crm-key`) and `x-crm-write: allow`, which says you mean to let this key change your CRM. In an AI assistant, tick "Let the assistant make changes" under Your CRM on the Ruuster connect screen. Changes are available for **Follow Up Boss** for now; other CRMs answer `400 crm_not_supported`. Ruuster never deletes a person, merges people or sends anything to a lead. See [Connect your CRM](https://help.ruuster.com/api/crm).

Typical use: find the people first ([Find engaged leads your CRM hasn't called](https://help.ruuster.com/api/leads-follow-up) gives each lead's `crm.personId`), then preview the change for those ids.

## Body

JSON, `Content-Type: application/json`, at most 64 KB.

| Name | Type | Required | Description |
|---|---|---|---|
| `action` | string | yes | `add_tags`, `remove_tags`, `set_stage`, `assign`, `add_note`, `add_task`, or `undo` (reverse a completed tag, stage or assignment change) |
| `request` | string | yes | The person's request in their own words, 3 to 1,000 characters. Kept with the change as the record of what was asked |
| `personIds` | string[] | unless `undo` | The CRM person ids to change, 1 to 500 (Follow Up Boss person ids) |
| `tags` | string[] | for tags | With `add_tags` or `remove_tags`: 1 to 10 tags, no commas. Adding keeps every other tag; removing takes away only these |
| `stage` | string | for stage | With `set_stage`: a stage name as your CRM lists it (see [Get your CRM's vocabulary](https://help.ruuster.com/api/crm-overview)). Trash isn't offered: it hides people from the CRM's own searches |
| `assignedUserId` | string | for assign | With `assign`: the CRM user id to assign the people to |
| `note.subject` | string | no | With `add_note`: up to 200 characters |
| `note.body` | string | for notes | With `add_note`: the note, up to 5,000 characters |
| `task.name` | string | for tasks | With `add_task`: the task, up to 200 characters |
| `task.type` | string | no | With `add_task`: `Follow Up` (the default), `Call`, `Email`, `Text`, `Appointment`, `Showing`, `Closing`, `Open House` or `Thank You` |
| `task.dueDate` | string | no | With `add_task`: `YYYY-MM-DD` |
| `task.assignedUserId` | string | no | With `add_task`: who the task is for. Default: each person's own assigned agent; people with no agent are skipped. No reminder is set |
| `undoChangeId` | string | for undo | With `undo`: the `changeId` of a completed change. Only what that change did is reversed, and anyone changed again since is left alone |
| `source` | object | no | How the people were chosen (for example the tool and its criteria), kept with the change |

## Response

| Field | Description |
|---|---|
| `changeId` | The change, `crmchg_…`. Pass it to Apply, Cancel or Get |
| `status` | `proposed`, or `cancelled` when there was nothing to change |
| `summary` | One sentence saying what the change does |
| `changesNothingYet` | Always `true`: a preview never writes to your CRM |
| `counts` | `requested`, `willChange`, `skipped` |
| `warnings[]` | What to consider before confirming |
| `people[]` | The first 50 people: `personId`, `name`, `status` (`pending` or `skipped`), `reason`, `before`, `after` |
| `expiresAt`, `earliestConfirmAt` | The window in which Apply accepts the confirmation |
| `nextStep` | What to do next, in words |

## Example request

cURL:

```bash
curl -s -X POST "https://api.ruuster.com/v1/crm/changes" \
  -H "x-api-key: $RUUSTER_API_KEY" \
  -H "x-email: $RUUSTER_EMAIL" \
  -H "x-crm-vendor: fub" \
  -H "x-crm-key: $CRM_API_KEY" \
  -H "x-crm-write: allow" \
  -H "Content-Type: application/json" \
  -d '{"action":"add_tags","personIds":["48213","50322"],"tags":["xyz"],"request":"Tag my leads who viewed homes in the last 7 days but haven't been called with xyz","source":{"tool":"leads_follow_up","criteria":{"from":"2026-10-15","engagement":"viewed","quietDays":30}}}'
```

Node.js:

```javascript
const res = await fetch("https://api.ruuster.com/v1/crm/changes", {
  method: "POST",
  headers: {
    "x-api-key": process.env.RUUSTER_API_KEY,
    "x-email": process.env.RUUSTER_EMAIL,
    "x-crm-vendor": "fub",
    "x-crm-key": process.env.CRM_API_KEY,
    "x-crm-write": "allow",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "action": "add_tags",
    "personIds": [
      "48213",
      "50322"
    ],
    "tags": [
      "xyz"
    ],
    "request": "Tag my leads who viewed homes in the last 7 days but haven't been called with xyz",
    "source": {
      "tool": "leads_follow_up",
      "criteria": {
        "from": "2026-10-15",
        "engagement": "viewed",
        "quietDays": 30
      }
    }
  }),
});
const body = await res.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message} (${body.requestId})`);
console.log(body.data);
```

Python:

```python
import os
import requests

res = requests.post(
    "https://api.ruuster.com/v1/crm/changes",
    headers={"x-api-key": os.environ["RUUSTER_API_KEY"], "x-email": os.environ["RUUSTER_EMAIL"], "x-crm-vendor": "fub", "x-crm-key": os.environ["CRM_API_KEY"], "x-crm-write": "allow"},
    json={
        "action": "add_tags",
        "personIds": ["48213", "50322"],
        "tags": ["xyz"],
        "request": "Tag my leads who viewed homes in the last 7 days but haven't been called with xyz",
        "source": {
            "tool": "leads_follow_up",
            "criteria": {
                "from": "2026-10-15",
                "engagement": "viewed",
                "quietDays": 30,
            },
        },
    },
    timeout=30,
)
body = res.json()
if not body["ok"]:
    raise RuntimeError(f'{body["error"]["code"]}: {body["error"]["message"]} ({body.get("requestId")})')
print(body["data"])
```

## Example response

```json
{
  "ok": true,
  "data": {
    "changeId": "crmchg_7Qm2VbX9kLp4RtYz",
    "status": "proposed",
    "action": "add_tags",
    "summary": "Add the tag \"xyz\" to 2 people in Follow Up Boss.",
    "changesNothingYet": true,
    "counts": { "requested": 2, "willChange": 2, "skipped": 0 },
    "warnings": [
      "Tags can start Follow Up Boss automations or action plans that are set to run on them.",
      "Ruuster sends nothing to these leads, but an automation or action plan this change triggers in Follow Up Boss could email or text them."
    ],
    "people": [
      { "personId": "48213", "name": "Sam Lee", "status": "pending", "reason": null, "before": { "tags": ["buyer"] }, "after": { "tags": ["buyer", "xyz"], "added": ["xyz"] } },
      { "personId": "50322", "name": "Dana Okafor", "status": "pending", "reason": null, "before": { "tags": [] }, "after": { "tags": ["xyz"], "added": ["xyz"] } }
    ],
    "expiresAt": "2026-10-22T19:15:00.000Z",
    "earliestConfirmAt": "2026-10-22T19:00:08.000Z",
    "nextStep": "Nothing has been changed yet. Show this preview to the person (the summary, the warnings and who is affected), ask whether they're sure, and only after they say yes, POST /crm/changes/crmchg_7Qm2VbX9kLp4RtYz/apply with { \"confirm\": true }. The preview expires in 15 minutes."
  },
  "meta": { "crm": { "vendor": "fub", "label": "Follow Up Boss", "source": "Follow Up Boss, live, with your API key", "calls": 3, "scope": "account" } }
}
```
