# Import contacts (CSV)

> POST /audiences/:audience_id/contacts/import — bulk-import contacts from a CSV.

`POST /audiences/:audience_id/contacts/import`

Import up to **10,000 contacts** (max **5 MB**) from a CSV. The CSV can be sent as a JSON body field or as a raw `text/csv` / `text/plain` body. A header row is optional — if present, column names are matched to contact fields (`email`, `first_name`, `last_name`, `unsubscribed`). By default, any other columns are **automatically registered as custom properties** (so `company`, `plan`, … survive and become `{{merge}}` tags). Pass `?create_properties=false` on the query string for strict mode, where only columns matching an already-registered property are kept and the rest are reported in `ignored_columns`.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `csv` | string | Yes | CSV text. The `email` column is always required. Other columns map to contact fields or custom properties. You can also `POST` a raw `text/csv` body instead of a JSON wrapper. |
| `on_conflict` | string | No | `upsert` (default) — update the existing contact when the email already exists. `skip` — leave it untouched. Can also be passed as a query parameter `?on_conflict=skip` when using a raw CSV body. |

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `create_properties` | boolean | No | Read from the **query string only** — a `create_properties` field in the JSON body is ignored. Defaults to `true`, so non-builtin CSV columns are auto-registered as string custom properties (up to 50 new per import) and no data is silently dropped. Pass `?create_properties=false` to keep only already-registered columns. |
| `segment_id` | string | No | Read from the **query string only**. Also add every imported email to this [segment](https://www.sendping.co/docs/segments/overview). The segment must be one of yours and must belong to the audience you are importing into — otherwise the request is rejected with `422 validation_error` before any contact is written. When set, the response carries a `segment_added` count. |

**Node.js**

```js
import { SendPing } from 'sendping';

const mb = new SendPing('mb_xxxxxxxxx');

const { data, error } = await mb.contacts.import({
  audienceId: 'AUDIENCE_ID',
  "csv": "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace"
});
console.log({ data, error });
```

**Ruby**

```ruby
require "sendping"

SendPing.api_key = "mb_xxxxxxxxx"

SendPing::Contacts.import({
  "audience_id": "AUDIENCE_ID",
  "csv": "email,first_name,last_name
steve@example.com,Steve,Wozniak
ada@example.com,Ada,Lovelace"
})
```

**PHP**

```php
<?php
require 'vendor/autoload.php';

use SendPing\SendPing;

$sendping = SendPing::client('mb_xxxxxxxxx');

$sendping->contacts->import([
  'audienceId' => "AUDIENCE_ID",
  'csv' => "email,first_name,last_name
steve@example.com,Steve,Wozniak
ada@example.com,Ada,Lovelace"
]);
```

**Python**

```python
import requests

res = requests.post(
    "https://www.sendping.co/api/audiences/AUDIENCE_ID/contacts/import",
    headers={"Authorization": "Bearer mb_xxxxxxxxx", "Content-Type": "application/json"},
    json={
  "csv": "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace"
},
)
print(res.json())
```

**Go**

```go
import "github.com/shekhu10/sendping-sdks/sendping-go"

client := sendping.NewClient("mb_xxxxxxxxx")

imported, err := client.Contacts.Import(&sendping.ImportContactsRequest{
    AudienceId: "AUDIENCE_ID",
    Csv:        "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace",
})
```

**Rust**

```rust
use sendping::{ImportCsvOptions, SendPing};

let mb = SendPing::new("mb_xxxxxxxxx");

let _imported = mb.contacts.import("AUDIENCE_ID", "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace", ImportCsvOptions::new()).await?;
```

**Java**

```java
import co.sendping.SendPing;
import co.sendping.SendPingResponse;
import co.sendping.requests.ImportContactsRequest;

SendPing sendping = new SendPing("mb_xxxxxxxxx");

ImportContactsRequest request = ImportContactsRequest.builder()
        .audienceId("AUDIENCE_ID")
        .csv("email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace")
        .build();

SendPingResponse response = sendping.contacts().importCsv(request);
```

**.NET**

```csharp
using SendPing;

ISendPing sendping = SendPingClient.Create("mb_xxxxxxxxx");

var resp = await sendping.ContactImportAsync(
    "AUDIENCE_ID",
    "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace");
```

**cURL**

```bash
curl -X POST 'https://www.sendping.co/api/audiences/AUDIENCE_ID/contacts/import' \
  -H 'Authorization: Bearer mb_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
  "csv": "email,first_name,last_name\nsteve@example.com,Steve,Wozniak\nada@example.com,Ada,Lovelace"
}'
```

### Response (201)

```json
{
  "object": "contact_import",
  "imported": 1,
  "updated": 1,
  "skipped": 0,
  "total": 2,
  "invalid_rows": 0,
  "limit_skipped": 0,
  "system_skipped": 0,
  "ignored_columns": [],
  "source_file": {
    "file_name": "contacts.csv",
    "storage_key": "contact-imports/9f3c1d7a4b2e6058c1d9e2f4/2026/06/23/20260623T172243000Z-7d3f0c1a-5b8e-4a92-9c07-1f6d2e4b8a35/contacts.csv",
    "archived": true
  },
  "contact_limit": {
    "plan": { "id": "free", "name": "Free" },
    "used_before": 12,
    "limit": 0,
    "remaining_before": null,
    "remaining_after": null,
    "limit_skipped": 0,
    "reached": false,
    "message": "Your plan includes unlimited stored contacts. Added 1 new contact and updated 1 existing contact. The complete original CSV is safely archived."
  }
}
```

`imported` counts new contacts; `updated` counts existing contacts merged by email. `skipped` includes invalid email rows and conflicts skipped by request. `total` counts source rows including duplicates, so it need not equal the sum of the other counts. `limit_skipped` and `system_skipped` are retained for compatibility and are zero: stored contacts are unlimited.

`ignored_columns` lists unregistered fields omitted when create_properties=false. source_file identifies the original S3 archive; send storage_key instead of csv to re-import without re-uploading. contact_limit remains a compatibility field: limit=0 and remaining=null mean unlimited storage. segment_added counts newly created memberships. Contacts and segment membership are committed atomically.

An empty CSV, malformed quoting, invalid UTF-8, ambiguous column names, or more than 50 custom property columns is rejected. Property values can contain up to 1,000 characters. Invalid email rows are counted and skipped. Inline API imports support up to 10,000 contacts; the direct-S3 dashboard path supports larger row counts within the file-size limit. Unknown audiences return 404.
