- Allow Duplicate Contact: off
- Find existing contacts based on: Phone
- Second preference for search: None
allowDuplicateContact: false and
contactUniqueIdentifiers: ["phone"]. From 2026-10-07 (PR #1890) first
installations turned Allow Duplicate Contact on instead; that changed on
2026-10-14.
Setup preserves every other Location setting, including Allow Duplicate
Opportunity. Sticky Contact forms can still reuse a Contact on a shared
browser; see the
CRM’s Sticky Contact guidance.
First installations
Setup requires a verified marketplace INSTALL notification and the Location’s credentials. Either may arrive first. A first OAuth authorization can wait for the notification; lazy credential discovery during an app open cannot start this setup. Existing installation history excludes older accounts, even after uninstall or credential cleanup. Successful setup records verification once. Later app opens, token refreshes, reauthorization, duplicate notifications and reinstalls do not reapply it. Uninstall cancels pending work. Setup still pending from before 2026-10-14 (which would have turned duplicates on) is cancelled when it next runs and writes nothing; the batch below covers those Locations. The setup reads current settings, sendscompanyId plus all returned settings
with the default applied, then reads again. A successful PUT without confirmed
readback is not configured. Before the first PUT, setup keeps the original
settings in installation history (settingsBeforeWriteJson); retries must
confirm those other settings. A mismatch stays a visible failure until staff
reconcile it in the CRM.
Installed Locations
Runscripts/contact-deduplication-batch.ts with an admin session
(FM_ADMIN_SESSION_TOKEN) and a Convex CLI login to production. This applies
the default everywhere, including Locations where the Operator chose otherwise.
- Pilot one Location first: run with
--locations <id>, apply it, and check that Location’s Contact settings in the CRM. - Run without
--apply. It is read only: it checks every installed Location and writescontact-deduplication-report/check-<time>.json, listing each as default, differs (with its current values) or unavailable (with the reason). - Review that report. Only differs Locations will change.
- Run again with
--apply <that report>. For each differs Location the server reads the settings again, sends them back with only the two deduplication settings changed, and reads once more to verify. Results go tocontact-deduplication-report/apply-<time>.json.
contactDeduplicationChanges table: a
started row with the two settings it replaces (beforeJson), saved before
the write, then the outcome with the admin and time. So any Location can be put
back, even if the CRM’s response was lost. Re-running is safe. If your admin
session expires mid-run, the report still lists every Location, with the ones
not reached marked not_run.
Unavailable means the owning agency hasn’t authorized locations.readonly
and locations.write, or the Location is no longer installed. Reconnect the
owning agency and re-run, or change the setting by hand in that Location’s CRM
Contact settings. Failed means the CRM didn’t confirm the change or another
setting changed at the same time; check that Location by hand.
Permissions
The official scope table requires agency access andlocations.write for
PUT /locations/.
Reading settings also requires locations.readonly. A Location token cannot
substitute for the owning agency’s token. Requests use the pinned SDK’s
locations.getLocation and locations.putLocation with that agency token.

