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

# Importing contacts

> Upload CSV, TSV or Excel, map columns, and let three-stage validation clean the list.

Bring an existing list into Senderz from a file. The importer is source-agnostic — the same engine handles CSV uploads and store back-fills — so validation, consent handling and status are identical everywhere.

## Supported formats

Upload **CSV, TSV, plain text, or Excel (`.xlsx` / `.xls`)** up to 10 MB. Excel workbooks use the first sheet; delimiters are auto-detected.

## Every upload is mapped

<Warning>
  Blind import is disabled on purpose. Every upload routes through the mapping
  step so a wrong column map can't reject every row (the classic
  Shopify/Excel import failure).
</Warning>

<Steps>
  <Step title="Pick a file">
    From **Contacts → Import**, or the contacts empty state.
  </Step>

  <Step title="Confirm the column map">
    Senderz parses the headers and auto-maps common ones — including Shopify
    consent columns (`Accepts Email Marketing`, `Accepts SMS Marketing`).
    Review and adjust before starting.
  </Step>

  <Step title="Import runs in the background">
    Progress and a per-row results table are shown on the import status page.
  </Step>
</Steps>

## Three-stage email validation

Each row's email is validated before it imports:

<Steps>
  <Step title="Syntax">
    Format check and lowercase normalisation.
  </Step>

  <Step title="Domain MX">
    A DNS MX lookup. No MX (or a null MX) means the domain can't receive mail
    — the row is rejected.
  </Step>

  <Step title="Quality">
    Disposable domains are rejected; role addresses (`info@`, `admin@`) are
    kept but flagged **risky**; in-file duplicates are removed.
  </Step>
</Steps>

Only **valid** and **risky** rows import. Rejects are counted, and the first reasons are surfaced. Reason codes: `syntax`, `no_mx`, `disposable`, `duplicate`, `role`, `undeliverable`.

<Note>
  Accept-all domains (like Gmail) return `unknown` at import — a non-existent
  Gmail mailbox surfaces later through the bounce → suppression pipeline, not
  at import time.
</Note>

## Consent on import is conservative

A row is opt-in **only** if the file carries a truthy consent signal. SMS is never auto-subscribed. Every consent change writes a consent-log entry with channel, state, source, IP and user agent.

<Info>
  Store-synced imports carry the provider's **own** consent timestamp (e.g.
  when the customer opted in on Shopify), not the import time — so your
  consent evidence stays accurate.
</Info>

## What completing an import does

* Marks contacts subscribed only where a consent signal was present.
* Completes the onboarding **Import** step when at least one row imports.
* Fires `contact_created` flows for genuinely new contacts (bulk import does **not** enrol into other triggers).

## Importing via the API

For programmatic contact creation, use the [Public API profiles endpoints](/en/api-reference/profiles) rather than file upload — `POST`/`PUT`/`PATCH /public/profiles`.
