Migration guide
Move your accounts, contacts, and deals into Pact from Salesforce, HubSpot, or Pipedrive — via the CSV import wizard or a native connector.
Bringing your data into Pact takes one of two paths: a one-time CSV import through the import wizard, or a native connector that authenticates against your source CRM and syncs records for you. Most teams do both — connect the CRM for the bulk of records, and use CSV for anything the connector doesn't cover.
Which path to choose
CSV import
The import wizard (/v1/admin/imports, surfaced in the app under Settings → Data import) handles the full lifecycle: draft, upload, map, preview, run. Supported entity types are account, contact, deal, journey, and custom_object (core/imports/schema.py); accounts land in companies, contacts in contacts, and deals in the deals pipeline.
- 1
Export from your current CRM
In Salesforce, use the Data Export / Reports export to produce a CSV per object (Accounts, Contacts, Opportunities). In HubSpot, use Export under each object's table view. One CSV per entity type.
- 2
Create a draft import
Pick the target entity type and dedup defaults. This creates a draft you can revisit before anything is written.
- 3
Upload the file
Multipart upload of your CSV. Pact parses the header row and a sample of values.
- 4
Auto-map columns
The wizard proposes a column → field mapping using a Claude-assisted + heuristic matcher (
/auto-map). Unmatched columns can be turned into custom fields in one step (/custom-fields). Review and confirm the mapping — you always get the final say. - 5
Preview
A first-ten-row preview shows exactly how your rows resolve against the mapping and dedup rules before any commit.
- 6
Start the import
Kick off the run. Progress and outcome are tracked per import; you can list past imports and inspect a single one by its
public_id.
Dedup is a first-class choice, not an afterthought
Every import carries a dedup policy so re-importing an updated export doesn't create duplicates. Confirm the dedup key (typically email for contacts, domain/name for accounts) at the mapping step. Import order matters: bring in accounts first, then contacts (so they link to the right company), then deals.
Native connectors
Connectors live under /v1/integrations and are provider-agnostic by design. Today the wired providers are Salesforce, HubSpot, and Pipedrive (api/routes/integrations.py).
Salesforce
Salesforce is the most complete connector — OAuth connect, scheduled + manual sync, and writeback:
POST /v1/integrations/salesforce/startreturns the authorize URL (supports sandbox orgs via asandboxflag).GET /v1/integrations/salesforce/callbackhandles the OAuth redirect; thestateparameter is single-use and tenant-scoped so a leaked link can't be replayed cross-tenant.GET /v1/integrations/salesforce/statusshows connection state, sync stats, recent runs, and your org's daily-API-call headroom (scraped fromSforce-Limit-Info).POST /v1/integrations/salesforce/synctriggers a manual "Sync now".DELETE /v1/integrations/salesforcedisconnects and clears stored tokens.
# 1. Begin the OAuth handshake
curl -X POST https://api.pact.place/v1/integrations/salesforce/start \
-H "Authorization: Bearer $PACT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sandbox": false}'
# → { "authorize_url": "https://login.salesforce.com/...", "state": "..." }
# 2. Open authorize_url in a browser, approve, land back on the callback.
# 3. Trigger the first pull:
curl -X POST https://api.pact.place/v1/integrations/salesforce/sync \
-H "Authorization: Bearer $PACT_TOKEN"
Token encryption is required
Connector OAuth tokens are stored encrypted with Fernet. The connect flow requires the platform's encryption key to be configured (AUTH_TOKEN_ENCRYPTION_KEY or JWT_SECRET); without it the connect step is refused rather than storing tokens in the clear.
HubSpot & Pipedrive
HubSpot and Pipedrive share the same integration store, status surface, and run history as Salesforce, and their sync clients (core/integrations/hubspot, core/integrations/pipedrive) map source objects onto the same Pact entities the CSV wizard targets. Their authentication and sync model differs from Salesforce, though:
- Auth is an API token, not three-legged OAuth. HubSpot connects with a Private App access token and Pipedrive with an API token, supplied through the in-app BYOK/settings flow. There is no
/start+/callbackOAuth handshake for these two — full user-consent OAuth is a follow-up (core/integrations/hubspot/__init__.pynotes the three-legged flow as a layered PR). - Sync is manual.
POST /v1/integrations/hubspot/syncandPOST /v1/integrations/pipedrive/syncare "Sync now" triggers with run history. The 30-minute background scheduler (core/integrations/salesforce/sync.pyrun_incremental) is Salesforce-only today.
Sync is import-oriented today
The connectors are built primarily for pulling data in (and, for Salesforce, writeback of specific fields). They are not a full bidirectional replication of every object and custom field. For objects a connector doesn't cover, export to CSV and use the import wizard.
After the migration
- Consent carries meaning here. Pact is consent-native — imported contacts should arrive with an honest consent state per channel. If your source CRM tracked opt-in/opt-out, map it during import rather than defaulting everyone to "subscribed."
- Verify record linkage. Spot-check that contacts resolved to the right accounts and deals to the right pipeline stages before you switch off the old system.
- Public IDs, not integer IDs. Pact exposes UUID
public_idvalues in URLs and API responses; the integer primary keys from your old CRM aren't reused. Bookmark and integrate againstpublic_id.