PPactDocs
Reference

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. 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. 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. 3

    Upload the file

    Multipart upload of your CSV. Pact parses the header row and a sample of values.

  4. 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. 5

    Preview

    A first-ten-row preview shows exactly how your rows resolve against the mapping and dedup rules before any commit.

  6. 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/start returns the authorize URL (supports sandbox orgs via a sandbox flag).
  • GET /v1/integrations/salesforce/callback handles the OAuth redirect; the state parameter is single-use and tenant-scoped so a leaked link can't be replayed cross-tenant.
  • GET /v1/integrations/salesforce/status shows connection state, sync stats, recent runs, and your org's daily-API-call headroom (scraped from Sforce-Limit-Info).
  • POST /v1/integrations/salesforce/sync triggers a manual "Sync now".
  • DELETE /v1/integrations/salesforce disconnects and clears stored tokens.
bash
# 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 + /callback OAuth handshake for these two — full user-consent OAuth is a follow-up (core/integrations/hubspot/__init__.py notes the three-legged flow as a layered PR).
  • Sync is manual. POST /v1/integrations/hubspot/sync and POST /v1/integrations/pipedrive/sync are "Sync now" triggers with run history. The 30-minute background scheduler (core/integrations/salesforce/sync.py run_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_id values in URLs and API responses; the integer primary keys from your old CRM aren't reused. Bookmark and integrate against public_id.