onComplete

onComplete: (rows: Record<string, unknown>[], meta: ImportMeta) => void | Promise<void>

type ImportMeta = {
  validRows: number;
  invalidRows: number;
  skippedRows?: number;
  fileType: "csv" | "xlsx" | "tsv" | "xlsb" | "ods" | "json";
  durationMs: number;
  columnCount: number;
  rowCount: number;
  sheetName?: string; // set for Excel imports
  headerRow: number; // 1-based spreadsheet row used as headers
  upsert?: { primaryKey: string; newRows: number; changedRows: number; missingKeyRows: number };
};

rows are objects keyed by field key, with coerced types (numbers as numbers, emails lowercased, dates as YYYY-MM-DD, booleans as booleans, URLs as normalized http/https hrefs, empty optional fields as null). Invalid rows are omitted.

Pass primaryKey and existingKeys (the host's prior ids) to receive meta.upsert counts for new vs already-known vs blank-key rows. There is no multi-file merge grid. Keys never leave the browser except through your onComplete.

If onComplete throws or returns a rejected Promise, the widget shows that message (the Error text when present) and re-enables Import. A successful callback still disables Import until the user goes back to mapping, so a second click cannot duplicate the send.

Your backend should re-check authorization and any server-side uniqueness against your database. Client validation is not a security boundary.

Related: Quickstart · React component · Import CSV to Postgres · Sessions