# Apply a change to your CRM

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

```http
POST https://api.ruuster.com/v1/crm/changes/{changeId}/apply
```

Step two of changing your CRM. Confirms a previewed change and makes it in your CRM, person by person, checking each one against what the CRM answers. Needs confirm=true from the person who previewed it. Large changes run in batches; call again with the same changeId to continue.

- 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)

Makes a change you previewed with [Preview a change to your CRM](https://help.ruuster.com/api/crm-change-preview). Only call it after the person has seen the preview and said yes: `confirm: true` records that they did.

The brakes, in order: the change must still be `proposed` (or `applying`, to continue); it must be confirmed by the same Ruuster login that previewed it; the first confirmation is refused within 8 seconds of the preview (`409 crm_change_too_soon`, so nothing can preview and apply in one breath without asking) and after 15 minutes (`409 crm_change_not_open`); the CRM key must be for the same CRM account the preview used; and Ruuster's own switch for CRM changes must be on.

Each person is then changed one at a time, spaced to stay inside Follow Up Boss's rate limits, and checked: Ruuster compares what Follow Up Boss answers (and, if needed, a fresh read) with the intended result, so a change the CRM silently ignored is reported as `failed`, never as done. One call works for about 17 seconds, roughly 35 tag, stage or assignment changes, or 15 notes. When people remain, `status` stays `applying` and `next` says to call again with the same `changeId` and `confirm: true`; the person's confirmation stands for the rest of that change. A rate limit or an outage pauses the change without losing it.

Needs your CRM key (`x-crm-vendor`, `x-crm-key`) and `x-crm-write: allow`.

## Path parameters

| Name | Type | Description |
|---|---|---|
| `changeId` | string | The change, `crmchg_…`, from Preview a change to your CRM |

## Body

JSON, `Content-Type: application/json`.

| Name | Type | Required | Description |
|---|---|---|---|
| `confirm` | boolean | yes | Must be `true`: the person saw this change's preview and said yes |

## Response

| Field | Description |
|---|---|
| `changeId`, `summary` | The change |
| `status` | `applying` (people remain), `completed`, or `completed_with_errors` |
| `counts` | `done`, `failed`, `skipped`, `remaining` across the whole change |
| `thisBatch[]` | The people this call handled: `personId`, `name`, `status` (`done` or `failed`), `reason` |
| `next` | What to do when people remain, or null |
| `pausedBy` | Why this batch stopped early (a CRM rate limit or outage), when it did |

## Example request

cURL:

```bash
curl -s -X POST "https://api.ruuster.com/v1/crm/changes/crmchg_7Qm2VbX9kLp4RtYz/apply" \
  -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 '{"confirm":true}'
```

Node.js:

```javascript
const res = await fetch("https://api.ruuster.com/v1/crm/changes/crmchg_7Qm2VbX9kLp4RtYz/apply", {
  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({
    "confirm": true
  }),
});
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/crmchg_7Qm2VbX9kLp4RtYz/apply",
    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={
        "confirm": True,
    },
    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": "completed",
    "summary": "Add the tag \"xyz\" to 2 people in Follow Up Boss.",
    "counts": { "done": 2, "failed": 0, "skipped": 0, "remaining": 0 },
    "thisBatch": [
      { "personId": "48213", "name": "Sam Lee", "status": "done", "reason": null },
      { "personId": "50322", "name": "Dana Okafor", "status": "done", "reason": null }
    ],
    "next": null
  },
  "meta": { "brokerageId": "41c73a1a-1dde-42e9-a9c9-0da74502b50e", "crm": { "vendor": "fub", "label": "Follow Up Boss", "writes": 2, "reads": 2 } }
}
```
