Skip to content

Bulk Contact Import

Scope

This page documents the current bulk contact import behavior observed in cred-api-commercial, cred-web-commercial, cred-model-api, and cred-ios-commercial on March 10, 2026.

Executive Summary

In this repo family, "bulk contact import" primarily means the Import pipeline implemented in cred-api-commercial. The canonical model is:

  1. Create an Import
  2. Create or infer ImportField mappings
  3. Read source rows into ImportRecords
  4. Process those records into Contacts
  5. Optionally add the created contacts to a target collection
  6. Optionally reconcile matches after the import

That pipeline is used by file imports, CRM full imports, Polytomic bulk syncs, and backend ingestion paths such as webhooks and Universal API. It is not the same as:

  • Manual CRM sync of already-existing contacts
  • iOS device contact sync
  • "Create Contacts" actions that batch createContact mutations
  • The async startBulkCreateContacts / bulkCreateContactsJob API used for non-import bulk contact creation

For the current non-import async bulk-create and cleanup path, see Bulk Create Contacts. For the higher-level decision framework, see Contact Ingestion Overview.

Repo Boundary

Repo Role in bulk contact import
cred-api-commercial Owns the import lifecycle, workers, REST/GraphQL endpoints, CRM import orchestration, webhook ingestion, and Universal API ingestion
cred-web-commercial Main first-party file import UI, field-mapping UI, imports pages, reconcile page, and Polytomic bulk sync wizard
cred-model-api Does not own the import engine; repo search only surfaced generated commercial API client/schema artifacts rather than native import logic
cred-ios-commercial Has device contact sync, but that flow creates/updates contacts directly and does not use Import / ImportField / ImportRecord

What "Bulk Import" Means Here

Core import objects

Object Purpose
Import Top-level import job. Tracks status, sourceType, entityType, fileId, identifier, templateName, listId, and row counters
ImportField One source column or remote field definition plus its mapping to a base field or custom field
ImportFieldRecord Sample values captured during setup to help field mapping and previews
ImportRecord One source row or payload stored for later processing into a local entity
Contact Final local entity. Imported contacts retain importId and importRowNo provenance

Lifecycle

The common lifecycle looks like this:

flowchart TD
    Trigger[Trigger or source event] --> Import[Import]
    Import --> Setup[Setup source fields]
    Setup --> ImportField[Create ImportField entries]
    ImportField --> Map[Map fields to CRED fields]
    Map --> ReadRows[Read or materialize source rows]
    ReadRows --> ImportRecord[ImportRecord]
    ImportRecord --> Process[ProcessNewImportRecordUseCase]
    Process --> Entity[Create or update local entity]
    Entity --> Complete[Mark import complete]
    Complete --> Post[Optional post-processing]

    Map --> CRMRead[DATA_SYNC_READ<br/>CRM-style read phase]
    CRMRead --> ImportRecord

Diagram: canonical import lifecycle. CRM-style sources use DATA_SYNC_READ for the read/materialize phase before ProcessNewImportRecordUseCase converts ImportRecords into local entities.

  1. A trigger creates or reuses an Import
  2. Setup determines the source fields and creates ImportFields
  3. A user or the system maps those fields to CRED fields
  4. Source rows are read or materialized into ImportRecords
  5. A processor turns each ImportRecord into a local entity
  6. The import is marked complete, and optional post-processing runs

This is the repo family's canonical definition of bulk import. It is heavier than the async client-driven bulk-create path because it is built around source-field modeling, record materialization, and optional reconcile/comparison surfaces rather than direct contact creation.

For file, webhook, and Universal API sources, ProcessNewImportRecordUseCase handles the per-record entity creation/update work.

For CRM-style sources, there is a two-phase data-sync model:

  1. DATA_SYNC_READ reads remote rows and persists ImportRecords
  2. CRM import record processors turn those rows into local entities

Contact-specific behavior

For contact imports specifically:

  • The contact template includes Name, FirstName, LastName, Email, PhoneNumber, Birthday, Country, Address, CompanyName, JobTitle, Note, LinkedinUrl, TwitterUrl, InstagramUrl, and CrmId
  • Template imports can auto-map common contact extras such as phone number, address, company name, job title, note, social URLs, and primary work email
  • If the import has a listId, successfully imported contacts are also added to that collection
  • Each ImportRecord can store entityId, matchingId, suggestedMatchingIds, and error, so the pipeline supports post-import matching and reconciliation workflows

Key code paths

  • cred-api-commercial/src/domain/import/entity/import.ts
  • cred-api-commercial/src/domain/import/entity/import-field.ts
  • cred-api-commercial/src/domain/import/entity/import-record.ts
  • cred-api-commercial/src/domain/contact/entity/contact.ts
  • cred-api-commercial/src/domain/import/usecase/records/process-new-import-record.ts
  • cred-api-commercial/src/domain/data-sync/usecase/process-crm-import-records.ts

Trigger Paths

1. Web file import

This is the main first-party contact import path today.

The flow is:

  1. The web import modal uploads a file through uploadCsvFileByCollection
  2. The frontend calls POST /upload-import-file
  3. UploadImportFileUseCase stores the file and creates an Import with sourceType = FILE and status = UPLOADING
  4. In non-local, non-test, non-CI environments, the import hook automatically queues SET_IMPORT_FIELDS
  5. The UI waits for an importUpdated event with status = PENDING
  6. If the upload was a template import, onSetImportFieldsExecuted auto-queues START_IMPORT_FILE_DATA
  7. Otherwise the user maps columns in the modal and then explicitly calls startImportFileData

Important nuances:

  • The current web modal uses the REST upload route, not the GraphQL uploadImportFile mutation
  • The REST route accepts template / templateName; that is how the contact-template shortcut is activated
  • The GraphQL upload mutation exists, but it does not expose templateName
  • Template imports can skip the manual mapping screen because field setup can auto-start processing once mappings exist

2. CRM authorization and full imports

Salesforce, HubSpot, and Microsoft Dynamics treat contact import as part of a broader CRM full-sync/import system.

The flow is:

  1. OAuth authorization succeeds
  2. The authorize use case queues SETUP_IMPORTS
  3. It also queues ENSURE_SYNC_SETTINGS
  4. It then queues DATA_SYNC_READ for Tier 0 entity types
  5. Contacts are imported later, after earlier dependency tiers finish

Contacts are Tier 2 entities in the current sync ordering, so contact import is not the first wave. The current dependency tiers are:

  • Tier 0: REMOTE_USER, OPPORTUNITY_PIPELINE, CUSTOM_ENTITY
  • Tier 1: ACCOUNT, CAMPAIGN, OPPORTUNITY_STAGE
  • Tier 2: CONTACT, CUSTOM_ENTITY_RECORD
  • Tier 3: OPPORTUNITY, CAMPAIGN_MEMBER
  • Tier 4: TASK, NOTE
flowchart LR
    Tier0Order["Tier 0"] --> Tier1Order["Tier 1"] --> Tier2Order["Tier 2"] --> Tier3Order["Tier 3"] --> Tier4Order["Tier 4"]

    subgraph Tier0["Tier 0 - Foundation"]
        direction TB
        REMOTE_USER[REMOTE_USER]
        OPPORTUNITY_PIPELINE[OPPORTUNITY_PIPELINE]
        CUSTOM_ENTITY[CUSTOM_ENTITY]
    end

    subgraph Tier1["Tier 1 - Core Entities"]
        direction TB
        ACCOUNT[ACCOUNT]
        CAMPAIGN[CAMPAIGN]
        OPPORTUNITY_STAGE[OPPORTUNITY_STAGE]
    end

    subgraph Tier2["Tier 2 - Contacts"]
        direction TB
        CONTACT[CONTACT]
        CUSTOM_ENTITY_RECORD[CUSTOM_ENTITY_RECORD]
    end

    subgraph Tier3["Tier 3 - Related"]
        direction TB
        OPPORTUNITY[OPPORTUNITY]
        CAMPAIGN_MEMBER[CAMPAIGN_MEMBER]
    end

    subgraph Tier4["Tier 4 - Activities"]
        direction TB
        TASK[TASK]
        NOTE[NOTE]
    end

    REMOTE_USER --> ACCOUNT
    OPPORTUNITY_PIPELINE --> OPPORTUNITY_STAGE
    CUSTOM_ENTITY --> CUSTOM_ENTITY_RECORD
    ACCOUNT --> CONTACT
    CAMPAIGN --> CAMPAIGN_MEMBER
    OPPORTUNITY_STAGE --> OPPORTUNITY
    CONTACT --> OPPORTUNITY
    CONTACT --> CAMPAIGN_MEMBER
    OPPORTUNITY --> TASK
    CONTACT --> NOTE

Diagram: CRM full-import dependency tiers from foundation entities through activities. Alt text: Tier 0 foundation objects feed Tier 1 core entities, Tier 2 contact entities depend on earlier tiers, Tier 3 related records depend on contacts and core entities, and Tier 4 activity records land last.

Operationally, CRM imports are more like "materialize remote CRM state into ImportRecords, then process those into local entities" than "upload a CSV."

3. Polytomic bulk sync

Polytomic is its own bulk path, but it still feeds the import framework.

The flow is:

  1. Web wizard collects source connection, schema, fields, and mapping info
  2. createPolytomicBulkSync creates the bulk sync and associated import artifacts
  3. The completion path runs setupImports
  4. setupImports configures fields, counts, and queues DATA_SYNC_READ
  5. The Polytomic data sync service reads from BigQuery-backed Polytomic datasets and processes records through the import/data-sync model

This path is not the legacy file importer, but it still lands in the same family of Import records and import-field mapping behavior.

4. Webhook ingestion

Webhook ingestion is backend-oriented bulk import.

The flow is:

  1. createWebhook creates a webhook and immediately creates a corresponding Import with sourceType = WEBHOOK
  2. External callers send JSON arrays to POST /universal-webhook/:ulid
  3. ProcessWebhookDataUseCase validates the payload and creates ImportRecords
  4. The import's rowsCount is incremented, and the import is moved back to UPLOADING if it was idle

Important nuance:

  • The route stores records synchronously and returns 202
  • In the code reviewed here, the webhook route itself does not enqueue SETUP_IMPORTS or START_IMPORT_FILE_DATA
  • That means some external worker trigger or manual orchestration still has to pick the import back up for field setup and record processing

5. Universal API ingestion

Universal API is another backend-oriented ingestion path that reuses the import model.

The flow is:

  1. createUniversalApiConfiguration creates the configuration and ensures there is an Import with sourceType = UNIVERSAL_API
  2. executeUniversalApiRequest executes the remote request
  3. Successful responses are converted into ImportRecords
  4. Arrays create one ImportRecord per item, single objects create one record, and primitives are wrapped into { value: ... }
  5. The existing import's rowsCount is updated

Important nuance:

  • As with webhooks, the reviewed execution path creates ImportRecords but does not itself enqueue follow-up import processing
  • The path clearly intends to share the downstream import machinery, but the automatic kickoff is not visible in the route/use case alone

6. Low-level GraphQL import creation

There is also a lower-level createImport GraphQL mutation that accepts entityType, sourceType, and identifier.

That appears to be a primitive "create or find an import shell" API rather than the main user-facing contact import entry point. Repo search did not find a first-party web or mobile caller for it.

Key code paths

  • cred-web-commercial/libs/shared/src/templates/modals/import-list/import-list-modal.tsx
  • cred-web-commercial/libs/shared/src/hooks/api.hook.ts
  • cred-web-commercial/libs/shared/src/sections/my-account/data-sync/bulk-sync-wizard.tsx
  • cred-api-commercial/src/api-routes/files-route.ts
  • cred-api-commercial/src/api-routes/webhook-route.ts
  • cred-api-commercial/src/graphql-api/import/resolvers/import-resolver.ts
  • cred-api-commercial/src/graphql-api/polytomic/resolvers/polytomic-resolver.ts
  • cred-api-commercial/src/domain/webhook/usecase/create-webhook-usecase.ts
  • cred-api-commercial/src/domain/webhook/usecase/process-webhook-data-usecase.ts
  • cred-api-commercial/src/domain/integration/usecase/api/create-universal-api-configuration-usecase.ts
  • cred-api-commercial/src/domain/integration/usecase/api/execute-universal-api-request-usecase.ts
  • cred-api-commercial/src/domain/integration/usecase/polytomic/process-polytomic-bulk-sync-completed.ts
  • cred-api-commercial/src/domain/integration/usecase/salesforce/authorize-salesforce-account.ts
  • cred-api-commercial/src/domain/integration/usecase/hubspot/authorize-hubspot-account.ts
  • cred-api-commercial/src/domain/integration/usecase/microsoft-dynamics/authorize-microsoft-dynamics-account.ts
  • cred-api-commercial/src/hooks/handlers/import.ts
  • cred-api-commercial/src/hooks/handlers/import-field.ts

Bulk Contact Features That Are Not Imports

These flows are easy to confuse with import because they can move many contacts at once, but they do not use the Import pipeline.

Manual CRM sync of existing contacts

The syncCRMManually mutation is an outbound sync path for existing records. It schedules DATA_SYNC_CREATE, DATA_SYNC_UPDATE, or DATA_SYNC_READ jobs against CRM-connected accounts.

This is not contact import in the file/import-record sense. It does not create a new Import, does not expose ImportField mapping, and does not use the file/webhook/Universal API processors.

Web "Create Contacts" batching

The people list UI can batch createContact calls for selected people who do not already have contact records.

That is also not import. It is a batched mutation loop that directly creates contacts.

iOS device contact sync

The iOS contact sync controller fetches device contacts, diffs them, then issues CreateContact, UpdateContact, and AddContactsToCollection mutations.

That is a bulk sync/create flow, but it bypasses the import model entirely.

Async bulk create contacts API

cred-api-commercial now also has a dedicated async bulk-create API centered on startBulkCreateContacts and bulkCreateContactsJob.

That path exists for client-driven "create a lot of contacts and show progress" behavior. It is still not import:

  • no Import
  • no ImportField
  • no ImportRecord
  • no field mapping UI
  • no reconcile/comparison flow
  • no import provenance on Contact

Instead, it creates contacts through the normal contact-create use case in a worker job and exposes state through pollable create/delete job queries. See Bulk Create Contacts.

Current mobile device-contact path

The current iOS device-contact flow that has been proven locally uses:

  • startBulkCreateContacts
  • bulkCreateContactsJob
  • startBulkDeleteCreatedContacts
  • bulkDeleteCreatedContactsJob

That means the currently validated mobile path is explicitly bulk create / bulk delete, not file import.

It is still reasonable to think about device contacts as an external source in the product sense. If the product later needs field mapping, persisted source rows, import provenance, or reconcile/comparison behavior for device contacts, the import pipeline remains the place to build that. It is just not the currently shipped/tested path.

Key code paths

  • cred-api-commercial/src/graphql-api/integration/resolvers/sync-settings-resolver.ts
  • cred-web-commercial/libs/shared/src/hooks/use-sync-contacts.ts
  • cred-web-commercial/libs/shared/src/sections/people/list-page/people-list-selection-bar.tsx
  • cred-ios-commercial/Packages/CREDUI/Sources/CREDUI/Services/ContactSync/ContactSyncController.swift
  • cred-ios-commercial/Packages/CREDAPI/Sources/CREDAPI/GraphQL/Contacts.graphql

Exposed Capabilities

File parsing and source ingestion

Current file-import capabilities include:

  • Accepted file types: CSV, Excel, and JSON
  • CSV delimiter handling: comma by default, with fallback detection for ; and tab-delimited files
  • CSV/Excel header normalization: empty header names are dropped, and duplicate CSV/Excel headers are suffixed to make them unique
  • JSON input shapes: top-level array, top-level object, or object containing data, records, or items
  • Empty files or empty JSON payloads are rejected

Contact template shortcuts

The contact template path currently supports:

  • Template download from /import-template and /import-json-template
  • Automatic field mapping for the known contact template columns
  • Automatic mapping into contact extra fields and primary work email
  • Auto-start after field setup when the template produced valid mappings

Mapping behavior

Current mapping behavior includes:

  • Mapping import fields to base data-description fields
  • Mapping import fields to existing custom fields
  • Marking one field per import as the unique identifier
  • Marking one field per import as the timestamp-tracking field
  • Marking one field per import as the record-name field

Processing behavior

The current processing path can:

  • Persist source rows before local entity creation
  • Create or update local contact entities through the import processors
  • Persist row-level errors on ImportRecord
  • Store match metadata such as matchingId and suggestedMatchingIds
  • Add imported contacts to a collection when listId is present
  • Trigger person-related post-processing after a completed contact import

UI surfaces

The current UI exposes:

  • Upload modal and field-mapping screen for file imports
  • Imports page and import subscriptions
  • Reconcile page for unresolved or unmatched rows
  • Comparison page behind the IMPORT_DATA_COMPARISON feature flag

Relevant UI code paths:

  • cred-web-commercial/apps/web-commercial/pages/my-account/imports/[id]/comparison/index.tsx
  • cred-web-commercial/apps/web-commercial/pages/my-account/imports/[id]/reconcile/index.tsx

Limitations and Caveats

1. The enum is broader than the actual implementation

ImportSourceTypeEnum includes many MERGE_* values and some other source names, but the current setup/data-sync factory only wires the following real bulk-import families:

  • FILE
  • WEBHOOK
  • UNIVERSAL_API
  • SALESFORCE
  • MICROSOFT_DYNAMICS
  • HUBSPOT
  • POLYTOMIC

So "supported in the enum" does not mean "supported as a working contact import path."

2. cred-model-api is not the owner of this feature

If a new feature needs changes to bulk contact import, the implementation work will primarily land in cred-api-commercial and cred-web-commercial, not cred-model-api.

3. CUSTOM_ENTITY is not directly importable

The upload flow rejects entityType = CUSTOM_ENTITY. The supported custom path is CUSTOM_ENTITY_RECORD, and it requires customEntityId.

4. A name field is effectively mandatory

The import processors look for a "name" field. For standard entities, required importable fields must be mapped. For custom entity record imports, required custom fields must be mapped.

5. REST and GraphQL file upload are not equivalent

The current GraphQL uploadImportFile mutation does not expose templateName, while the REST upload route does. That matters for contact template auto-mapping and auto-start behavior.

6. Field-unmapping rules are source-sensitive

For file, webhook, and Universal API imports, fields cannot be unmapped while the import is in UPLOADING or PROCESSING. CRM-style imports are looser because canMapFields is implemented differently for those sources.

7. Sample/preview storage is capped

Current sample limits are:

  • File setup stores sample values only for the first 10,000 rows
  • CRM read sampling stores up to 100 values per field

Those caps matter if a future feature expects full-source sampling or validation across the entire dataset.

8. CRM setup does not currently auto-create custom fields

The CRM setup code explicitly avoids auto-creating custom fields because it created too much unnecessary processing. CRM field setup mainly maps to existing base fields and existing templated custom fields.

9. Webhook payloads are capped

The webhook ingestion path enforces:

  • Maximum payload size: 10 MB
  • Maximum record count: 10,000

10. Automatic file setup is disabled in local/test/CI

onUploadImportFileExecuted short-circuits in CI, local, and test environments. That means the fully automatic upload -> field setup flow is not a faithful reproduction of production behavior in those environments.

11. Backend-oriented ingestion paths have an orchestration gap

In the code reviewed here:

  • Webhook ingestion creates ImportRecords and moves the import back to UPLOADING, but does not itself enqueue setup/start processing
  • Universal API execution creates ImportRecords, but also does not visibly enqueue the next import-processing step

If a new feature depends on those paths auto-processing contacts, confirm the external scheduler or worker trigger before building on that assumption.

12. The comparison page is currently synthetic

The comparison page exists, but the current page implementation builds seeded placeholder comparison rows from aggregate counts instead of reading actual row-by-row imported values and matched values. It should not be treated as a production-grade import diff viewer yet.

13. CRM contact imports are dependency-ordered

Contacts are Tier 2 entities in the CRM full-sync ordering. If a new feature expects contacts to import first, that expectation will conflict with the current orchestration model.

14. Current batch defaults are conservative

Some of the active defaults in code are relatively small:

  • File/webhook record processing defaults to concurrency around 20 unless config overrides it
  • DEFAULT_IMPORT_RECORD_PROCESSING_BATCH_SIZE for CRM read/process orchestration is currently 20

That matters for throughput expectations on large bulk imports.

End Matrix

Path Uses Import pipeline? Trigger / entry point Processing shape Contact-specific capability Main caveats
Web file import Yes Web modal -> POST /upload-import-file -> startImportFileData File stored -> fields set -> rows read into ImportRecords -> records processed into Contacts Best-supported first-party contact import path; supports template auto-mapping and optional collection add REST path is the real template-enabled path; local/test/CI disables auto field setup; GraphQL upload lacks templateName
CRM authorization full import Yes Salesforce / HubSpot / Microsoft Dynamics OAuth authorization SETUP_IMPORTS + DATA_SYNC_READ Phase 1 + CRM record processing Phase 2 Imports contacts from connected CRM and persists import metadata Contacts are Tier 2, not first; CRM setup does not auto-create custom fields; batch defaults are small
Polytomic bulk sync Yes createPolytomicBulkSync via web wizard Bulk sync setup -> setupImports -> DATA_SYNC_READ from Polytomic datasets -> import/data-sync processors Supports contact-oriented bulk ingestion from Polytomic-connected data Separate from file import; depends on Polytomic datasets and wizard mapping
Webhook ingestion Yes, but backend-oriented createWebhook + POST /universal-webhook/:ulid Incoming JSON arrays become ImportRecords on an existing import Can ingest contact-shaped JSON payloads into the import framework 10 MB / 10,000-record cap; reviewed route does not itself queue setup/start processing
Universal API ingestion Yes, but backend-oriented createUniversalApiConfiguration + executeUniversalApiRequest Remote response becomes ImportRecords on an existing import Can ingest contact-shaped API responses as arrays or objects Reviewed execution path does not visibly queue the follow-up import processor
Low-level createImport mutation Partially GraphQL createImport Creates or finds an import shell by entityType, sourceType, and identifier Useful as a primitive building block Repo search did not find a first-party UI caller; not a full user-facing flow
Async bulk create contacts API No startBulkCreateContacts -> bulkCreateContactsJob Async worker loop around CreateContactUseCase, optional collection add, pollable job state Best fit for iOS device contacts and client-driven bulk-create UX; supports tracking markers for testing/cleanup Not import; no mapping/reconcile/provenance; polling is the source of truth; total runtime is still dominated by existing contact creation and matching
Manual CRM sync of existing contacts No syncCRMManually Schedules CRM create/update/read jobs for existing records Can sync existing contacts out to CRM Not import; no ImportField mapping, no file/webhook processors
Web "Create Contacts" batching No People list selection bar Batches createContact mutations Converts people into contacts in bulk Not import; no import records, no mapping, no reconcile
iOS device contact sync No ContactSyncController.startSync() Fetch device contacts -> diff -> CreateContact / UpdateContact / AddContactsToCollection Bulk-create or update contacts from device address book Not import; bypasses Import, ImportField, and import reconcile flows