Express CSV Logo

Importer SDK

@expresscsv/sdk is centered around the CSVImporter class. You configure it once with new CSVImporter(options), then call open() when you want to start an import.

new CSVImporter(options)

class CSVImporter<TSchema> {
  constructor(options: SDKOptions<TSchema>);
  open(options: OpenOptions<Infer<TSchema>>): void;
  close(
    reason?: "user_close" | "cancel" | "complete" | "error"
  ): Promise<void>;
  restart(newOptions?: Partial<SDKOptions<TSchema>>): Promise<void>;
  getStatus(): ImporterStatus;
  getMode(): ImporterMode;
  getIsReady(): boolean;
  getIsOpen(): boolean;
  getCanRestart(): boolean;
  getLastError(): Error | null;
  getConnectionStatus(): boolean;
  getStatusSnapshot(): {
    status: ImporterStatus;
    mode: ImporterMode;
    isReady: boolean;
    isOpen: boolean;
    canRestart: boolean;
    hasError: boolean;
    lastError: Error | null;
    connectionStatus: boolean;
  };
  subscribeToStatusChanges(
    listener: (status: ImporterStatus) => void
  ): () => void;
}

Constructor options

OptionDescriptionTypeRequiredDefault
schemaSchema definition created with x.row()SchemaYes
getSessionTokenAsync callback that asks your backend for a short-lived import session token() => Promise<string>Yes
importNamespaceStable namespace string your app assigns to this importer configuration. Keep it the same for the same workflow; use a different value for different importers.stringYes
titleTitle shown in the importer headerstringNo
preloadPreload the importer in a hidden iframe for instant displaybooleanNotrue
debugEnable debug loggingbooleanNofalse
themeTheme variable overrides (see Styling)ThemeNo
colorMode'light', 'dark', or 'system'ColorModePrefNo
customCSSCustom CSS injected into the importerstringNo
fontsCustom font sourcesRecord<string, FontSource>No
stepDisplayStep indicator style'progressBar', 'segmented', or 'numbered'No'progressBar'
previewSchemaBeforeUploadShow expected columns before uploadbooleanNotrue
columnMatchingConfigure managed column matching or provide a custom matcher{ type: "managed"; exact?: boolean; caseInsensitive?: boolean; normalized?: boolean; inference?: boolean } | { type: "custom"; columnMatchHandler: (...) => Promise<...> }Noundefined
promptedEditsEnable managed prompted edits or provide a custom edit handler{ type: "managed" } | { type: "custom"; promptedEditHandler: (...) => Promise<...> }Noundefined
templateDownloadOffer downloadable template filesTemplateDownloadOptions<TSchema>No
sessionRecoveryEnable recovered sessions with either local recovery or a custom adapter implementing get, set, and removeSessionRecoveryOptionsNo
localeLocale string overrides (see Localization)DeepPartial<ExpressCSVLocaleInput>No
disableStatusStepSkip the success/error status screenbooleanNo

Instance surface

MemberDescription
open(options)Starts an import. Requires onData on options.
close(reason?)Closes the importer (preload mode hides the iframe for reuse; normal mode tears down).
restart(newOptions?)Resets and reopens when getCanRestart() is true (typically after preload close).
getStatus()Current ImporterStatus from the SDK.
getMode()ImporterMode.PRELOAD or ImporterMode.NORMAL.
getIsReady()true when status is READY or OPEN.
getIsOpen()true while the importer is open.
getCanRestart()true when the instance can run restart().
getLastError()Most recent error, if any.
getConnectionStatus()Whether the iframe RPC connection is up.
getStatusSnapshot()Snapshot of status helpers (same fields the getters expose).
subscribeToStatusChanges(listener)Subscribe to ImporterStatus changes; returns an unsubscribe function.

While the iframe is loading or an import session is starting, getStatus() is INITIALIZING or OPENING. Use getStatus() or subscribeToStatusChanges() instead of a single initialization flag.


open(options)

Options passed to open().

OptionDescriptionTypeRequired
onDataCallback for each chunk. Call next() after your backend accepts the current chunk.(chunk: RecordsChunk<T>, next: () => void) => void or Promise<void>Yes
chunkSizeDelivery packet size. Defaults to { unit: "kb", value: 500 }. kb uses decimal kilobytes (1 KB = 1000 bytes). Use { unit: "kb", value: 500 } for KB or { unit: "rows", value: 500 } for row counts. Zero or negative values send all records in one chunk.ChunkSizeNo
onCompleteCalled when all chunks have been processed(context: { sessionId: string; deliveryId: string }) => voidNo
onCancelCalled when the user cancels the import(context: { sessionId: string }) => voidNo
onErrorCalled when an error occurs. deliveryId is present for delivery failures.(error: Error, context: { sessionId: string; deliveryId?: string }) => voidNo
onOpenCalled when the importer opens(context: { sessionId: string }) => voidNo
onStepChangeCalled when the importer step changes(stepId: ImporterStep, previousStepId?: ImporterStep) => voidNo

RecordsChunk<T>

interface RecordsChunk<T> {
  records: T[];
  totalChunks: number;
  chunkIndex: number;
  totalRecords: number;
  sessionId: string;
  deliveryId: string;
}
PropertyDescriptionType
recordsValidated records for this chunk, typed from your schemaT[]
totalChunksTotal number of chunks in the deliverynumber
chunkIndex0-based index of the current chunk within this deliverynumber
totalRecordsTotal records across all chunksnumber
sessionIdStable ID for this import run. Use it to group chunks and lifecycle callbacks.string
deliveryIdStable ID for the delivery created when the user finishes the import. Use it with sessionId and chunkIndex when saving each chunk to your import chunk staging table (a database table or store).string

ImporterStatus

The importer moves through these statuses during its lifecycle:

StatusDescription
UNINITIALIZEDInitial status before the importer iframe is created
INITIALIZINGIframe is loading and establishing a connection
READYImporter is loaded, preloaded, and ready to be opened
OPENINGopen() has been called; creating an import session
OPENImporter is visible and the user is interacting with it
CLOSINGImporter is in the process of closing
RESETTINGImporter is resetting for a new import
ERRORAn error occurred (check getLastError())
DESTROYEDImporter has been torn down (normal mode after close)

Typical flow: UNINITIALIZEDINITIALIZINGREADYOPENINGOPENCLOSINGREADY


ImporterStep

The importer step IDs passed to onStepChange:

Step IDDescription
'upload'File upload and optional schema preview
'select-sheet'Sheet selection (multi-sheet files only)
'select-header'Header row selection
'match-columns'Map CSV columns to schema fields
'match-options'Map option values for select/multiselect fields
'review'Review, edit, and validate data before import

Preloading

  • preload: true (default): the importer iframe loads in the background so open() displays instantly with no loading screen
  • preload: false: the iframe is created only when open() is called; the user sees a brief loading indicator
  • Use preload: false when the importer is rarely used and you want to avoid the upfront iframe cost

Exported Types

All of these are importable from @expresscsv/sdk:

TypeDescription
Infer<TSchema>Extracts the row type from a schema
SDKOptionsOptions for new CSVImporter()
InferCSVImporterHelper to infer CSVImporter type from a schema
OpenOptionsOptions for open()
ImportSessionContext{ sessionId: string } passed to session-scoped lifecycle callbacks
ImportDeliveryContext{ sessionId: string; deliveryId: string } passed to delivery-scoped lifecycle callbacks
ImportErrorContext{ sessionId: string; deliveryId?: string } passed to onError
RecordsChunkChunk delivered to onData
SessionRecoveryOptionsConfiguration for recovered sessions
LocalSessionRecoveryOptionsBuilt-in browser recovery configuration
CustomSessionRecoveryOptionsCustom recovery adapter configuration
SessionRecoveryKeyOpaque key generated for a recovered session lookup
RecoveredSessionWrapper type for a recovered session payload in the importer
RecoveredSessionDataRow data and column keys for a recovered session
SchemaFieldNameUnion of string keys inferred from a schema
ManagedColumnMatchingOptions{ type: "managed"; ... }
CustomColumnMatchingOptions{ type: "custom"; columnMatchHandler }
ColumnMatchingOptionsManaged or custom column matching
ColumnMatchHandlerCustom column matching handler alias
ColumnMatchHandlerResultResult type for custom column matching
ManagedPromptedEditsOptions{ type: "managed" }
CustomPromptedEditsOptions{ type: "custom"; promptedEditHandler }
PromptedEditsOptionsManaged or custom prompted edits
PromptedEditHandlerCustom prompted edits handler alias
PromptedEditResultResult type for custom prompted edits
ColumnMatchingFnLow-level custom column matching handler type
ColumnMatchingParamsInput passed to a custom column matching handler
ColumnMatchingResultResult returned by a custom column matching handler
ColumnMatchingSourceColumnSource column payload for matching
ColumnMatchingTargetFieldTarget field payload for matching
ColumnMatchA single source-to-field match
PromptedEditsFnLow-level custom prompted edits handler type
PromptedEditsParamsInput passed to a custom prompted edits handler
PromptedEditsResultResult returned by a custom prompted edits handler
PromptedEditsFieldField metadata for prompted edits
PromptedEditsRowRow payload for prompted edits
PromptedEditsChangeA single add/update/remove change
ThemeTheme variable overrides (single or dual-mode)
ColorModePref'light', 'dark', or 'system'
FontSourceFont source (Google or custom URL)
TailwindThemeVarsTheme variable names
ImporterStatusImporter lifecycle status enum
ImporterModeImporter preload mode enum
ImporterStepImporter step IDs
TemplateDownloadConfigTemplate download configuration
TemplateDownloadOptionsTemplate options with schema-typed example rows
TemplateDownloadFormat'csv' or 'xlsx'
TemplateDownloadExampleRowExample row type for templates
ExpressCSVLocaleInputFull locale type
DeepPartialDeep partial utility type
ImportCancelledErrorError thrown when import is cancelled
ExTypeSchema type helper from @expresscsv/fields
ExBaseDefBase definition type from @expresscsv/fields
xSchema builder (value, not a type)

Example

const importer = new CSVImporter({
  schema,
  getSessionToken: async () => {
    const response = await fetch("/your-api/import/session", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });
    const { token } = await response.json();
    return token;
  },
  importNamespace: "user-import",
});

console.log(importer.getStatus());

await importer.close("user_close");