> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safefetch.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# CRM Contact Update

> Safely update CRM contact records from AI agents — with deduplication to prevent double-writes and sync mode for immediate confirmation.

## The problem

An AI agent that writes directly to a CRM can fire duplicate updates if it retries, or silently overwrite fields the moment it decides to — no confirmation, no audit record.

```ts theme={null}
// ❌ Direct HubSpot call — no dedup, no record, fires immediately
await fetch(`https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`, {
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}` },
  body: JSON.stringify({ properties: { email: "new@example.com", phone: "+15550001234" } }),
});
```

If the agent retries (network hiccup, timeout), HubSpot gets two identical writes. And if the agent is wrong about the contact ID, the damage is immediate and silent.

## The SafeFetch solution

Wrap the CRM call in `safeFetch()` with a `dedupe` key so duplicate requests are collapsed into one, and use `sync: true` to wait for confirmation before telling the user the update succeeded.

## Full example

```ts theme={null}
import { safeFetch } from "safefetch";

// Derive a stable dedupe key from the operation — same contact + same fields
// within 24 h will return the original action instead of firing again.
const dedupeKey = `crm-update:${contactId}:email+phone`;

const action = await safeFetch({
  url: `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`,
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: {
    properties: {
      email: "new@example.com",
      phone: "+15550001234",
    },
  },
  dedupe: dedupeKey,
  // Wait for HubSpot to respond before returning
  sync: true,
  syncTimeout: 15_000, // 15 s
});

if (action.status === "completed" && action.response_code === 200) {
  console.log("Contact updated:", action.id);
} else if (action.deduplicated) {
  console.log("Duplicate request — original action:", action.id);
} else {
  console.log("Update failed:", action.last_error);
}
```

`safeFetch()` returns once the PATCH to HubSpot has completed (or failed). `action.deduplicated === true` means the same write was already in flight — no second request was made.

## How deduplication works

<Steps>
  <Step title="First call goes through">
    SafeFetch stores the action and dispatches the PATCH to HubSpot. The `dedupe` key is recorded alongside it.
  </Step>

  <Step title="Retry or duplicate call arrives">
    If your agent retries within 24 hours using the same `dedupe` key, SafeFetch returns the original action object immediately. HubSpot is not contacted a second time.
  </Step>

  <Step title="Check `deduplicated`">
    Inspect `action.deduplicated` to know whether you got a fresh result or the cached one. Either way, `action.status` and `action.response_code` reflect the true outcome.
  </Step>
</Steps>

## Using sync mode

`sync: true` polls the action until it reaches a terminal status (`completed`, `failed`, or `cancelled`) and returns the final object. This is useful when the downstream workflow needs confirmation before proceeding.

```ts theme={null}
const action = await safeFetch({
  url: `https://api.salesforce.com/services/data/v59.0/sobjects/Contact/${contactId}`,
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SF_ACCESS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: {
    Email: "new@example.com",
    Phone: "+15550001234",
  },
  sync: true,
});

// Guaranteed to be in a terminal status here
console.log(action.status);        // "completed" or "failed"
console.log(action.response_code); // e.g. 204 (Salesforce returns 204 on success)
```

## Checking status later

If you don't use `sync: true`, inspect the action at any point using its ID:

```ts theme={null}
const action = await safeFetch.get("act_Xk9mP2nQ4rT1vW8sY");

if (action.status === "completed") {
  console.log(`Update confirmed in ${action.duration_ms}ms`);
} else if (action.status === "failed") {
  console.log("Failed after", action.attempts, "attempts:", action.last_error);
}
```

## Adding a human approval gate

For high-stakes field changes (ownership transfers, account merges, subscription downgrades), add `approve: true` to require sign-off before the write reaches your CRM:

```ts theme={null}
const action = await safeFetch({
  url: `https://api.hubapi.com/crm/v3/objects/contacts/${contactId}`,
  method: "PATCH",
  headers: { Authorization: `Bearer ${process.env.HUBSPOT_TOKEN}` },
  body: { properties: { hs_lead_status: "UNQUALIFIED" } },
  approve: true,
  notify: { email: "sales-ops@yourcompany.com" },
  dedupe: `crm-disqualify:${contactId}`,
});

console.log(action.status); // "awaiting_approval"
```

See [Human Approval](/guides/human-approval) for the full approval workflow.

<Note>
  `sync: true` and `approve: true` cannot be combined. Use `sync: true` for automated writes where you need an immediate result, and `approve: true` when a human must review the change first.
</Note>
