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

# Importing Contacts

> Bulk-import contacts from a CSV file or inline JSON rows, with column mapping, dedupe control, and per-row results.

## Endpoints

```bash theme={null}
POST /v1/contacts/imports
POST /v1/contacts/imports/{session_id}/preview
POST /v1/contacts/imports/{session_id}/execute
GET  /v1/contacts/imports/{import_run_id}
GET  /v1/contacts/imports/{import_run_id}/results
GET  /v1/contacts/imports/{import_run_id}/export
```

Imports run asynchronously: start a session (upload a CSV or send rows inline), optionally preview the detected column mappings, execute, then poll the run until it completes. Every row gets an individually reported outcome.

<Info>
  For small synchronous batches (up to 100 records), use [`POST /v1/contacts/upsert`](/api-reference/managing-contacts) instead. Imports shine for large files, dedupe control, and auditable per-row results.
</Info>

## Start an Import

```bash theme={null}
POST /v1/contacts/imports
```

Two mutually exclusive modes, selected by `source`:

### Upload mode

| Field          | Type       | Required | Notes                           |
| -------------- | ---------- | -------- | ------------------------------- |
| `source`       | `"upload"` | Yes      | Requests a presigned upload URL |
| `file_name`    | `string`   | Yes      | Original CSV file name          |
| `content_type` | `string`   | No       | Defaults to `text/csv`          |

Returns an `import_session_id` plus a presigned S3 `upload_url` (valid for 1 hour) — `PUT` your CSV bytes directly to it, then continue to preview/execute.

### Inline rows mode

| Field       | Type     | Required | Notes                                            |
| ----------- | -------- | -------- | ------------------------------------------------ |
| `source`    | `"rows"` | Yes      | Send data inline, no file handling               |
| `file_name` | `string` | No       | Label used in run history                        |
| `rows`      | `array`  | Yes      | Up to `1000` objects; keys become column headers |

Request bodies are capped at **5 MB**. Larger datasets should use upload mode.

```bash theme={null}
curl --request POST \
  --url 'https://api.tented.ai/v1/contacts/imports' \
  --header 'Authorization: Bearer tented_your_api_key' \
  --header 'Content-Type: application/json' \
  --data '{
    "source": "rows",
    "file_name": "crm-sync.json",
    "rows": [
      {"Email": "jane@example.com", "First Name": "Jane", "Company": "Acme"},
      {"Email": "sam@example.com", "First Name": "Sam", "Company": "Globex"}
    ]
  }'
```

## Preview an Import

```bash theme={null}
POST /v1/contacts/imports/{session_id}/preview
```

Parses the header plus sample rows and returns auto-detected `mappings` (source header → target field), per-column sample validation `issues`, the org's `available_fields`, and `field_creation_candidates` for unmapped columns. Optional for headless runs — you can pass mappings straight to execute.

Proposed mappings never send two columns to the same field. When two columns match one field, the column matched by its header name wins over an AI suggestion, then the earlier column wins. The other column is left unmapped with a `reason` naming the column that claimed the field, so it shows up in `field_creation_candidates`. For example, in a Shopify customer export, `Phone` maps to `phone` and `Default Address Phone` is left for a custom field.

## Execute an Import

```bash theme={null}
POST /v1/contacts/imports/{session_id}/execute
```

| Field                          | Type      | Required | Notes                                                                                                                                                                       |
| ------------------------------ | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mappings`                     | `array`   | Yes      | `{header, target_field, target_kind, overwrite_behavior?}` per column. Each field can be the target of only one column; mapping two columns to the same field returns `400` |
| `duplicate_strategy`           | `string`  | No       | `update` (default) merges into matching contacts; `skip` leaves them untouched                                                                                              |
| `overwrite_blank_only`         | `boolean` | No       | When `true`, mapped fields default to filling blanks only                                                                                                                   |
| `list_ids`                     | `array`   | No       | Static list IDs (≤ 500) every imported contact is added to                                                                                                                  |
| `created_field_definition_ids` | `array`   | No       | Custom fields bulk-created for this session's unmapped columns                                                                                                              |

Rows matching an existing contact (normalized email or phone) follow `duplicate_strategy`; per-column `overwrite_behavior` (`overwrite` | `skip_if_filled`) controls whether filled values are replaced. Returns `202` with an `import_run_id`.

## Poll a Run

```bash theme={null}
GET /v1/contacts/imports/{import_run_id}
```

`status` moves `queued → running → completed | failed`, with `created_count`, `updated_count`, `skipped_count`, `error_count`, and chunk-level progress.

## Per-Row Results

```bash theme={null}
GET /v1/contacts/imports/{import_run_id}/results?status=failed&page=1&limit=100
```

Paginated outcomes (`limit` ≤ 250), filterable by `status` (`created` | `updated` | `skipped` | `failed`); failed rows include `field_issues` with error codes. Row numbers count the header as line 1.

```bash theme={null}
GET /v1/contacts/imports/{import_run_id}/export
```

Streams a CSV of `skipped` and `failed` rows with reasons (`Content-Disposition: attachment`). Use the paginated results endpoint for full result sets.

## Common Errors

| Status | Meaning                                                                 |
| ------ | ----------------------------------------------------------------------- |
| `400`  | Invalid mappings, unknown target field, or malformed rows               |
| `404`  | Unknown `session_id` / `import_run_id` (sessions expire after 24 hours) |
| `409`  | Session already executed                                                |
| `413`  | Inline rows body over 5 MB                                              |
