---
title: "Import Contacts from CSV"
slug: csv-contacts-import
description: "Import millions of contacts from a CSV in a single upload, with an API, AI column mapping, and full SDK coverage."
created_at: "2026-06-24"
updated_at: "2026-06-24"
image: https://cdn.resend.com/posts/csv-contacts-import.jpg
humans: ["lucas-costa", "vitor-capretz", "giovana-yahiro"]
---

For many, the entry point for Contacts is bulk uploading data from a previous provider or a third-party tool like a CRM.

While we've long supported CSV imports, the experience was limited. Today, we're announcing a new CSV importer that's **faster**, more **flexible**, and includes **full API coverage**.

- 300% increased file size
- 10x import speed improvement
- Full API or SDK support
- Intelligent column matching

<YouTube videoId="Lgsj5rWBRPE" />

## New dashboard experience

We've rebuilt the dashboard experience from the ground up.

1. Upload a CSV as large as 200MB.
2. Columns are automatically mapped to properties.
3. Confirm your import.

<video
  src="https://cdn.resend.com/posts/csv-contacts-import-1.mp4"
  autoPlay
  loop
  muted
  playsInline
  className="extraWidth"
/>

The dashboard uses AI to automatically match your CSV columns to existing properties and suggest new custom properties, so you spend less time wiring up fields by hand.

## Programmatic imports

You can now create and track imports programmatically with the [Contacts Import API](/docs/api-reference/contacts/create-contact-import) using the API or your preferred SDK.

<CodeTabs codeHeight={475}>

```nodejs
import { readFile } from 'node:fs/promises';
import { Resend } from 'resend';

const resend = new Resend('re_xxxxxxxxx');

const file = new Blob([await readFile('contacts.csv')], {
  type: 'text/csv',
});

const { data, error } = await resend.contacts.imports.create({
  file,
  columnMap: {
    email: 'Email',
    firstName: 'First Name',
    lastName: 'Last Name',
    properties: {
      plan: {
        column: 'Plan',
        type: 'string',
      },
    },
  },
  onConflict: 'upsert',
  segments: [{ id: '78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6' }],
});
```

```php
$resend = Resend::client('re_xxxxxxxxx');

$resend->contacts->imports->create([
  'file' => fopen('contacts.csv', 'r'),
  'column_map' => [
    'email' => 'Email',
    'first_name' => 'First Name',
    'last_name' => 'Last Name',
    'properties' => [
      'plan' => [
        'column' => 'Plan',
        'type' => 'string',
      ],
    ],
  ],
  'on_conflict' => 'upsert',
  'segments' => [
    ['id' => '78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6'],
  ],
]);
```

```python
import resend

resend.api_key = "re_xxxxxxxxx"

with open("contacts.csv", "rb") as f:
    file_content = f.read()

params: resend.Contacts.Imports.CreateParams = {
    "file": file_content,
    "column_map": {
        "email": "Email",
        "first_name": "First Name",
        "last_name": "Last Name",
        "properties": {
            "plan": {
                "column": "Plan",
                "type": "string",
            },
        },
    },
    "on_conflict": "upsert",
    "segments": [{"id": "78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6"}],
}

resend.Contacts.Imports.create(params)
```

```ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

params = {
  file: File.read("contacts.csv"),
  column_map: {
    "email" => "Email",
    "first_name" => "First Name",
    "last_name" => "Last Name",
    "properties" => {
      "plan" => {
        "column" => "Plan",
        "type" => "string"
      }
    }
  },
  on_conflict: "upsert",
  segments: [{ id: "78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6" }]
}

Resend::Contacts::Imports.create(params)
```

```go
package main

import (
	"os"

	"github.com/resend/resend-go/v3"
)

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	file, err := os.ReadFile("contacts.csv")
	if err != nil {
		panic(err)
	}

	params := &resend.CreateContactImportRequest{
		File:       file,
		Filename:   "contacts.csv",
		OnConflict: "upsert",
		ColumnMap: map[string]any{
			"email":      "Email",
			"first_name": "First Name",
			"last_name":  "Last Name",
			"properties": map[string]any{
				"plan": map[string]any{
					"column": "Plan",
					"type":   "string",
				},
			},
		},
		Segments: []resend.ContactImportSegment{{Id: "78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6"}},
	}

	client.Contacts.Imports.Create(params)
}
```

```rust
use resend_rs::types::{
  ContactImportColumnMap, ContactImportOnConflict, ContactImportPropertyMapping,
  ContactImportPropertyType, CreateContactImportOptions,
};
use resend_rs::{Resend, Result};
use std::fs::File;

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let column_map = ContactImportColumnMap::new()
    .with_email("Email")
    .with_first_name("First Name")
    .with_last_name("Last Name")
    .with_property(
      "plan",
      ContactImportPropertyMapping {
        column: "Plan".to_owned(),
        r#type: ContactImportPropertyType::String,
      },
    );

  let options = CreateContactImportOptions::new()
    .with_column_map(column_map)
    .with_on_conflict(ContactImportOnConflict::Upsert)
    .with_segment("78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6");

  let file = File::open("contacts.csv").unwrap();
  let _import = resend.contacts.create_import(file, options).await?;

  Ok(())
}
```

```java
import com.resend.Resend;
import com.resend.core.exception.ResendException;
import com.resend.services.contacts.model.ContactImportColumnMap;
import com.resend.services.contacts.model.ContactImportPropertyMapping;
import com.resend.services.contacts.model.ContactImportSegmentReference;
import com.resend.services.contacts.model.CreateContactImportOptions;
import com.resend.services.contacts.model.CreateContactImportResponseSuccess;
import java.io.File;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        ContactImportColumnMap columnMap = ContactImportColumnMap.builder()
                .email("Email")
                .firstName("First Name")
                .lastName("Last Name")
                .property("plan", ContactImportPropertyMapping.builder()
                        .column("Plan")
                        .type("string")
                        .build())
                .build();

        CreateContactImportOptions params = CreateContactImportOptions.builder()
                .file(new File("contacts.csv"))
                .columnMap(columnMap)
                .onConflict("upsert")
                .segments(new ContactImportSegmentReference("78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6"))
                .build();

        CreateContactImportResponseSuccess data = resend.contacts().imports().create(params);
    }
}
```

```dotnet
// .NET SDK does not currently support creating contact imports.
```

```curl
curl -X POST 'https://api.resend.com/contacts/imports' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -F 'file=@contacts.csv;type=text/csv' \
     -F 'column_map={"email":"Email","first_name":"First Name","last_name":"Last Name","properties":{"plan":{"column":"Plan","type":"string"}}}' \
     -F 'on_conflict=upsert' \
     -F 'segments=[{"id":"78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6"}]'
```

```cli
resend contacts imports create \
  --file contacts.csv \
  --column-map '{"email":"Email","firstName":"First Name","lastName":"Last Name","properties":{"plan":{"column":"Plan","type":"string"}}}' \
  --on-conflict upsert \
  --segment-id 78e7a5c6-9a91-4c63-9d1f-3b9c0b5b9ab6
```

</CodeTabs>

We've also added full support for imports to our MCP and CLI.

<video
  src="https://cdn.resend.com/posts/csv-contacts-import-2.mp4"
  autoPlay
  loop
  muted
  playsInline
  className="extraWidth"
/>

## Automatic contact properties

When importing Contacts via the dashboard or API, we will create Contact Properties for you automatically if they don't yet exist for your team.

In the dashboard, you can reject suggestions or provide your own.

<video
  src="https://cdn.resend.com/posts/csv-contacts-import-3.mp4"
  autoPlay
  loop
  muted
  playsInline
  className="extraWidth"
/>

In the API, provide your `properties` in the column map and new Contact Properties will be created on import.

<CodeTabs codeHeight={260}>

```nodejs
const { data, error } = await resend.contacts.imports.create({
  file,
  columnMap: {
    email: 'Email',
    --highlight-start
    properties: {
      plan: {
        column: 'Plan',
        type: 'string',
      },
    },
    --highlight-end
  },
});
```

```php
$resend->contacts->imports->create([
  'file' => fopen('contacts.csv', 'r'),
  'column_map' => [
    'email' => 'Email',
    --highlight-start
    'properties' => [
      'plan' => [
        'column' => 'Plan',
        'type' => 'string',
      ],
    ],
    --highlight-end
  ],
]);
```

```python
params: resend.Contacts.Imports.CreateParams = {
    "file": file_content,
    "column_map": {
        "email": "Email",
        --highlight-start
        "properties": {
            "plan": {
                "column": "Plan",
                "type": "string",
            },
        },
        --highlight-end
    },
}

resend.Contacts.Imports.create(params)
```

```ruby
Resend::Contacts::Imports.create(
  file: File.read("contacts.csv"),
  column_map: {
    "email" => "Email",
    --highlight-start
    "properties" => {
      "plan" => {
        "column" => "Plan",
        "type" => "string"
      }
    },
    --highlight-end
  }
)
```

```go
params := &resend.CreateContactImportRequest{
	File: []byte("email,plan\nuser@example.com,pro"),
	ColumnMap: map[string]any{
		"email": "Email",
		--highlight-start
		"properties": map[string]any{
			"plan": map[string]any{
				"column": "Plan",
				"type":   "string",
			},
		},
		--highlight-end
	},
}

client.Contacts.Imports.Create(params)
```

```rust
let column_map = ContactImportColumnMap::new()
  .with_email("Email")
  --highlight-start
  .with_property(
    "plan",
    ContactImportPropertyMapping {
      column: "Plan".to_owned(),
      r#type: ContactImportPropertyType::String,
    },
  );
  --highlight-end

let options = CreateContactImportOptions::new().with_column_map(column_map);

resend.contacts.create_import(file, options).await?;
```

```java
ContactImportColumnMap columnMap = ContactImportColumnMap.builder()
        .email("Email")
        --highlight-start
        .property("plan", ContactImportPropertyMapping.builder()
                .column("Plan")
                .type("string")
                .build())
        --highlight-end
        .build();

CreateContactImportOptions params = CreateContactImportOptions.builder()
        .file(new File("contacts.csv"))
        .columnMap(columnMap)
        .build();

resend.contacts().imports().create(params);
```

```dotnet
// .NET SDK does not currently support creating contact imports.
```

```curl
curl -X POST 'https://api.resend.com/contacts/imports' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -F 'file=@contacts.csv;type=text/csv' \
     --highlight-start
     -F 'column_map={"email":"Email","properties":{"plan":{"column":"Plan","type":"string"}}}'
     --highlight-end
```

```cli
resend contacts imports create \
  --file contacts.csv \
  --highlight-start
  --column-map '{"email":"Email","properties":{"plan":{"column":"Plan","type":"string"}}}'
  --highlight-end
```

</CodeTabs>

## Update or skip existing contacts

Choose what happens when a Contact already exists via the API.

Set `on_conflict` to `skip` to leave existing contacts untouched, or `upsert` to update them, which makes bulk updates as easy as a re-import.

<CodeTabs codeHeight={220}>

```nodejs
const { data, error } = await resend.contacts.imports.create({
  file,
  columnMap: {
    email: 'Email',
    firstName: 'First Name',
  },
  --highlight-start
  onConflict: 'upsert',
  --highlight-end
});
```

```php
$resend->contacts->imports->create([
  'file' => fopen('contacts.csv', 'r'),
  'column_map' => [
    'email' => 'Email',
  ],
  --highlight-start
  'on_conflict' => 'upsert',
  --highlight-end
]);
```

```python
params: resend.Contacts.Imports.CreateParams = {
    "file": file_content,
    "column_map": {
        "email": "Email",
    },
    --highlight-start
    "on_conflict": "upsert",
    --highlight-end
}

resend.Contacts.Imports.create(params)
```

```ruby
Resend::Contacts::Imports.create(
  file: File.read("contacts.csv"),
  column_map: {
    "email" => "Email"
  },
  --highlight-start
  on_conflict: "upsert"
  --highlight-end
)
```

```go
params := &resend.CreateContactImportRequest{
	File: []byte("email\nuser@example.com"),
	ColumnMap: map[string]any{
		"email": "Email",
	},
	--highlight-start
	OnConflict: "upsert",
	--highlight-end
}

client.Contacts.Imports.Create(params)
```

```rust
let options = CreateContactImportOptions::new()
  --highlight-start
  .with_on_conflict(ContactImportOnConflict::Upsert);
  --highlight-end

resend.contacts.create_import(file, options).await?;
```

```java
CreateContactImportOptions params = CreateContactImportOptions.builder()
        .file(new File("contacts.csv"))
        --highlight-start
        .onConflict("upsert")
        --highlight-end
        .build();

resend.contacts().imports().create(params);
```

```dotnet
// .NET SDK does not currently support creating contact imports.
```

```curl
curl -X POST 'https://api.resend.com/contacts/imports' \
     -H 'Authorization: Bearer re_xxxxxxxxx' \
     -F 'file=@contacts.csv;type=text/csv' \
     --highlight-start
     -F 'on_conflict=upsert'
     --highlight-end
```

```cli
resend contacts imports create \
  --file contacts.csv \
  --highlight-start
  --on-conflict upsert
  --highlight-end
```

</CodeTabs>

## Import process tracking

Imports run in the background. Fetch any import programmatically to watch its status and get a live breakdown of what was created, updated, skipped, or failed.

<CodeTabs codeHeight={260}>

```nodejs
import { Resend } from "resend";

const resend = new Resend("re_xxxxxxxxx");

const { data } = await resend.contacts.imports.get(
  "479e3145-dd38-476b-932c-529ceb705947",
);
```

```php
$resend = Resend::client('re_xxxxxxxxx');

$resend->contacts->imports->get(
  '479e3145-dd38-476b-932c-529ceb705947'
);
```

```python
import resend

resend.api_key = "re_xxxxxxxxx"

resend.Contacts.Imports.get("479e3145-dd38-476b-932c-529ceb705947")
```

```ruby
require "resend"

Resend.api_key = "re_xxxxxxxxx"

Resend::Contacts::Imports.get("479e3145-dd38-476b-932c-529ceb705947")
```

```go
package main

import "github.com/resend/resend-go/v3"

func main() {
	client := resend.NewClient("re_xxxxxxxxx")

	client.Contacts.Imports.Get("479e3145-dd38-476b-932c-529ceb705947")
}
```

```rust
use resend_rs::{Resend, Result};

#[tokio::main]
async fn main() -> Result<()> {
  let resend = Resend::new("re_xxxxxxxxx");

  let _import = resend
    .contacts
    .get_import("479e3145-dd38-476b-932c-529ceb705947")
    .await?;

  Ok(())
}
```

```java
import com.resend.Resend;
import com.resend.core.exception.ResendException;
import com.resend.services.contacts.model.GetContactImportResponseSuccess;

public class Main {
    public static void main(String[] args) throws ResendException {
        Resend resend = new Resend("re_xxxxxxxxx");

        GetContactImportResponseSuccess data = resend
                .contacts()
                .imports()
                .get("479e3145-dd38-476b-932c-529ceb705947");
    }
}
```

```dotnet
// .NET SDK does not currently support retrieving contact imports.
```

```curl
curl -X GET 'https://api.resend.com/contacts/imports/479e3145-dd38-476b-932c-529ceb705947' \
     -H 'Authorization: Bearer re_xxxxxxxxx'
```

```cli
resend contacts imports get 479e3145-dd38-476b-932c-529ceb705947
```

</CodeTabs>

The response includes a `status` along with specific `counts` for more details.

```json
{
  "object": "contact_import",
  "id": "479e3145-dd38-476b-932c-529ceb705947",
  "status": "completed",
  "created_at": "2026-05-15 18:32:37.823+00",
  "completed_at": "2026-05-15 18:33:42.916+00",
  "counts": {
    "total": 1200,
    "created": 800,
    "updated": 300,
    "skipped": 75,
    "failed": 25
  }
}
```

## Get started

We've continued to invest in our Contacts experience, adding [Contact Properties](/docs/dashboard/audiences/properties), [Segments](/docs/dashboard/segments/introduction), and [Topics](/docs/dashboard/topics/introduction) to enable control, personalization, and better organization.

We trust this enhanced experience for bulk importing makes it easier to trust and use Resend. [View the docs](/docs/dashboard/audiences/contacts) for more details.
