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:
- Create an
Import - Create or infer
ImportFieldmappings - Read source rows into
ImportRecords - Process those records into
Contacts - Optionally add the created contacts to a target collection
- 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
createContactmutations - The async
startBulkCreateContacts/bulkCreateContactsJobAPI 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.
- A trigger creates or reuses an
Import - Setup determines the source fields and creates
ImportFields - A user or the system maps those fields to CRED fields
- Source rows are read or materialized into
ImportRecords - A processor turns each
ImportRecordinto a local entity - 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:
DATA_SYNC_READreads remote rows and persistsImportRecords- 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, andCrmId - 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
ImportRecordcan storeentityId,matchingId,suggestedMatchingIds, anderror, so the pipeline supports post-import matching and reconciliation workflows
Key code paths
cred-api-commercial/src/domain/import/entity/import.tscred-api-commercial/src/domain/import/entity/import-field.tscred-api-commercial/src/domain/import/entity/import-record.tscred-api-commercial/src/domain/contact/entity/contact.tscred-api-commercial/src/domain/import/usecase/records/process-new-import-record.tscred-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:
- The web import modal uploads a file through
uploadCsvFileByCollection - The frontend calls
POST /upload-import-file UploadImportFileUseCasestores the file and creates anImportwithsourceType = FILEandstatus = UPLOADING- In non-local, non-test, non-CI environments, the import hook automatically queues
SET_IMPORT_FIELDS - The UI waits for an
importUpdatedevent withstatus = PENDING - If the upload was a template import,
onSetImportFieldsExecutedauto-queuesSTART_IMPORT_FILE_DATA - 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
uploadImportFilemutation - 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:
- OAuth authorization succeeds
- The authorize use case queues
SETUP_IMPORTS - It also queues
ENSURE_SYNC_SETTINGS - It then queues
DATA_SYNC_READfor Tier 0 entity types - 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:
- Web wizard collects source connection, schema, fields, and mapping info
createPolytomicBulkSynccreates the bulk sync and associated import artifacts- The completion path runs
setupImports setupImportsconfigures fields, counts, and queuesDATA_SYNC_READ- 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:
createWebhookcreates a webhook and immediately creates a correspondingImportwithsourceType = WEBHOOK- External callers send JSON arrays to
POST /universal-webhook/:ulid ProcessWebhookDataUseCasevalidates the payload and createsImportRecords- The import's
rowsCountis incremented, and the import is moved back toUPLOADINGif 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_IMPORTSorSTART_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:
createUniversalApiConfigurationcreates the configuration and ensures there is anImportwithsourceType = UNIVERSAL_APIexecuteUniversalApiRequestexecutes the remote request- Successful responses are converted into
ImportRecords - Arrays create one
ImportRecordper item, single objects create one record, and primitives are wrapped into{ value: ... } - The existing import's
rowsCountis 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.tsxcred-web-commercial/libs/shared/src/hooks/api.hook.tscred-web-commercial/libs/shared/src/sections/my-account/data-sync/bulk-sync-wizard.tsxcred-api-commercial/src/api-routes/files-route.tscred-api-commercial/src/api-routes/webhook-route.tscred-api-commercial/src/graphql-api/import/resolvers/import-resolver.tscred-api-commercial/src/graphql-api/polytomic/resolvers/polytomic-resolver.tscred-api-commercial/src/domain/webhook/usecase/create-webhook-usecase.tscred-api-commercial/src/domain/webhook/usecase/process-webhook-data-usecase.tscred-api-commercial/src/domain/integration/usecase/api/create-universal-api-configuration-usecase.tscred-api-commercial/src/domain/integration/usecase/api/execute-universal-api-request-usecase.tscred-api-commercial/src/domain/integration/usecase/polytomic/process-polytomic-bulk-sync-completed.tscred-api-commercial/src/domain/integration/usecase/salesforce/authorize-salesforce-account.tscred-api-commercial/src/domain/integration/usecase/hubspot/authorize-hubspot-account.tscred-api-commercial/src/domain/integration/usecase/microsoft-dynamics/authorize-microsoft-dynamics-account.tscred-api-commercial/src/hooks/handlers/import.tscred-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:
startBulkCreateContactsbulkCreateContactsJobstartBulkDeleteCreatedContactsbulkDeleteCreatedContactsJob
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.tscred-web-commercial/libs/shared/src/hooks/use-sync-contacts.tscred-web-commercial/libs/shared/src/sections/people/list-page/people-list-selection-bar.tsxcred-ios-commercial/Packages/CREDUI/Sources/CREDUI/Services/ContactSync/ContactSyncController.swiftcred-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, oritems - Empty files or empty JSON payloads are rejected
Contact template shortcuts
The contact template path currently supports:
- Template download from
/import-templateand/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
matchingIdandsuggestedMatchingIds - Add imported contacts to a collection when
listIdis 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_COMPARISONfeature flag
Relevant UI code paths:
cred-web-commercial/apps/web-commercial/pages/my-account/imports/[id]/comparison/index.tsxcred-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:
FILEWEBHOOKUNIVERSAL_APISALESFORCEMICROSOFT_DYNAMICSHUBSPOTPOLYTOMIC
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 toUPLOADING, 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_SIZEfor CRM read/process orchestration is currently20
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 |