> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frontic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CSV Import

The CSV Import pulls **CSV files from an SFTP server you control** and turns each row into a record on a Data Feed. Reach for it when your source system is a nightly export, a partner-supplied feed, or any process that drops a file on a server but doesn't expose an API.

<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "0.75rem", marginTop: "1.5rem", marginBottom: "2rem" }}>
  <div style={{ padding: "0.75rem 1rem", border: "1px solid var(--card-border, rgba(0,0,0,0.08))", borderRadius: "0.625rem" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6, marginBottom: "0.25rem" }}>
      <Icon icon="key" iconType="solid" /> Auth
    </div>

    <div style={{ fontSize: "0.875rem", fontWeight: 500 }}>SFTP credentials</div>
  </div>

  <div style={{ padding: "0.75rem 1rem", border: "1px solid var(--card-border, rgba(0,0,0,0.08))", borderRadius: "0.625rem" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6, marginBottom: "0.25rem" }}>
      <Icon icon="arrows-rotate" iconType="solid" /> Update methods
    </div>

    <div style={{ fontSize: "0.875rem", fontWeight: 500 }}>Trigger · Polling</div>
  </div>

  <div style={{ padding: "0.75rem 1rem", border: "1px solid var(--card-border, rgba(0,0,0,0.08))", borderRadius: "0.625rem" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6, marginBottom: "0.25rem" }}>
      <Icon icon="boxes-stacked" iconType="solid" /> Resources
    </div>

    <div style={{ fontSize: "0.875rem", fontWeight: 500 }}>Content (one record per row)</div>
  </div>
</div>

The CSV Import follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers the SFTP-specific configuration and the row-to-record translation.

## Install and setup

There's no plugin to install. You provide an SFTP endpoint and Frontic does the rest.

<Steps>
  <Step title="Decide where files will live" icon="folder">
    Pick a directory on an SFTP server Frontic can reach. Best practice: a dedicated user with read+delete on a single base path, not your whole filesystem.
  </Step>

  <Step title="Add the integration in Frontic" icon="plug">
    In Frontic admin, **Integrations → Add → CSV Import**. Enter the SFTP host, port, username, password, and base path.
  </Step>

  <Step title="Test the connection" icon="check">
    Frontic opens a connection, lists the base path, and confirms it reached the server. Fix credentials or firewall rules until this test passes.
  </Step>

  <Step title="Add channels and feeds" icon="rss">
    Configure at least one channel (declares which project locales the rows resolve into) and one Data Feed per filename pattern you want to ingest. See the sections below.
  </Step>
</Steps>

## Connection settings

<ParamField path="Use SFTP" type="boolean" required>
  Currently the only supported transport. Reserved for future protocols.
</ParamField>

<ParamField path="Host" type="string" required>
  SFTP server hostname or IP.
</ParamField>

<ParamField path="Port" type="string" default="22">
  SFTP port.
</ParamField>

<ParamField path="Username" type="string" required>
  SFTP user. Use a dedicated account scoped to the import directory.
</ParamField>

<ParamField path="Password" type="string" required>
  SFTP password. Stored encrypted; redacted in logs.
</ParamField>

<ParamField path="Base path" type="string" default="/">
  The root directory the connector lists from. Feed-level configurations are evaluated relative to this path.
</ParamField>

The settings panel shows a **Connection Status** indicator. When the SFTP connection is broken, it surfaces the reason returned by the server — so you can tell whether credentials are missing, the host is unreachable, or auth was rejected. Re-test after rotating credentials.

## Channels

Unlike connectors with source-side segmentation (Shopware sales channels, Storyblok spaces), CSV files aren't natively segmented. The CSV Import's channels exist purely to declare which **project translations** each CSV row should be resolved into. Each row gets the same payload across every translation on the channel — see "Same row across all locales" under [Good to know](#good-to-know) for the implication.

Per channel, the connector stores:

<ParamField path="Channel Name" type="string" required>
  A label for the channel in Frontic.
</ParamField>

<ParamField path="Available Translations" type="multiselect" required>
  Project locale keys the CSV's rows resolve into. Every row gets the same payload across all of these.
</ParamField>

<ParamField path="Fallback Translation" type="select" required>
  The translation used as fallback for missing values.
</ParamField>

For multi-locale data via CSV with locale-specific copy, see "Same row across all locales" below for the workaround.

## Data Feeds

The CSV Import exposes a single resource type — **Content** (one record per row). Each Data Feed binds a filename pattern under your base path; you can have many feeds per integration if you have many file types (products, customers, categories, …).

The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For CSV Import specifically:

* **Updates step** — Polling and manual trigger are supported. The Ingest API isn't used.
* **No feed Refresh.** Each polling pass reprocesses every matching file. Use **Cleanup files** so processed files aren't picked up next time, or version filenames so the regex matches only new ones.

### Feed configuration

<ParamField path="Start dir" type="string">
  Subdirectory under the base path where this feed's files live. Empty = base path itself.
</ParamField>

<ParamField path="Delimiter" type="string" default=";">
  Field delimiter. Allowed values: `,`, `;`, `\t` — anything else is rejected.
</ParamField>

<ParamField path="Filename" type="string" required>
  Regex pattern matched against filenames in the start dir. For example `^products_\d{8}\.csv$` matches `products_20260426.csv`.
</ParamField>

<ParamField path="Use lockfile" type="boolean">
  When on, before reading any file the connector checks for a lock file in the same folder. If a lock file exists, the run aborts — no files are processed. Useful for partner uploads that drop a `.lock` while writing.
</ParamField>

<ParamField path="Lockfile" type="string">
  *(Sub-config of **Use lockfile**.)* The exact filename to look for as the lock file. Defaults to empty (no check).
</ParamField>

<ParamField path="Sort files and folders" type="boolean">
  When on, files are processed in filename order. Useful when filenames carry timestamps and order matters.
</ParamField>

<ParamField path="Folder sort" type="string" default="ASC">
  *(Sub-config of **Sort files and folders**.)* Either `ASC` or `DESC`.
</ParamField>

<ParamField path="Use custom identifier" type="boolean">
  When on, the record's Source ID is built from one or more named columns instead of an `id` column. Multi-column identifiers are joined with `-`.
</ParamField>

<ParamField path="Custom identifiers" type="string[]">
  *(Sub-config of **Use custom identifier**.)* Column names to use as the Source ID.
</ParamField>

<ParamField path="Custom parent identifiers" type="string[]">
  *(Sub-config of **Use custom identifier**.)* Column names whose values become each record's parent identifier — joined with `-` when you list more than one. Use this when rows represent child records (e.g. product variants) and a column carries the parent's key. Leave empty when records have no parent.
</ParamField>

<ParamField path="Cleanup files" type="boolean" default="false">
  Delete each file after a successful run.
</ParamField>

<ParamField path="Cleanup folders" type="boolean" default="false">
  *(Sub-config of **Cleanup files**.)* After cleaning files, also remove directories that ended up empty.
</ParamField>

## What the data looks like

### One row, one record

The header row of the CSV becomes the field keys; each subsequent row becomes a record. Whitespace in headers is trimmed. The Source ID for a record is taken from the `id` column unless you've set **Custom identifiers**.

```csv theme={"theme":"css-variables"}
id;name;sku;price_eur;price_usd
1001;Cotton Tee;TEE-001;19.99;21.99
1002;Linen Shirt;SHI-002;49.99;54.99
```

The two rows above produce two records on the feed: `1001` and `1002`. As CSV files come in, the feed's schema fills in automatically from the column headers — no need to declare fields up front in the wizard's Schema step.

## Good to know

* **CSV only.** No JSON, no XML, no Excel. If you need other formats, convert upstream or use the [Custom Integration](/connectors/custom) and the [Ingest API](/reference/ingest-api).
* **Same row across all locales.** A CSV row produces the **same payload for every locale** on the channel. The CSV Import doesn't read column-name suffixes like `name_de` / `name_en` natively. To split by locale: ingest the fields raw and resolve translations in your [Value Composer](/data-integration/value-composer), or split your data into per-locale files and feeds and map them via the Data Sync.
* **No real-time.** The connector polls. Webhook-style "the moment a file arrives" delivery isn't supported — set the polling cadence for your acceptable lag.
* **Every run re-reads every matching file.** There's no file-level "I've seen this before" check — if a file stays in the directory, it's parsed again on the next run. Rows that haven't materially changed still short-circuit at the storage layer ([change detection](/connectors/overview#change-detection)), so downstream impact is contained — but each row counts as an API Update at intake. Use **Cleanup files** to delete processed files, or version filenames so the regex picks up only the new ones.

## Related

<CardGroup cols={2}>
  <Card title="Custom Integration" icon="paintbrush-fine" href="/connectors/custom">
    For source systems with their own API — push records directly via the Ingest API.
  </Card>

  <Card title="Ingest API" icon="upload" href="/reference/ingest-api">
    The HTTP push path when SFTP polling isn't the right fit.
  </Card>
</CardGroup>
