# Import contacts (array)

> POST /contacts/batch — bulk-import contacts into a sending domain from a JSON array.

`POST /contacts/batch`

Import up to **10,000 contacts** into one of your **sending domains** in a single request by sending a JSON array. Contacts are domain-scoped, so pass the `domain` they belong to alongside the rows. Invalid rows (missing or malformed `email`) are counted as skipped and never cause the whole request to fail.

> **Note:** This is the recommended way to add many contacts. One batch request does the whole import in a single pass — it acquires your account's contact-limit lock once and writes the rows in chunks. Looping [POST /contacts](https://www.sendping.co/docs/api/contacts-create) once per contact does the opposite: every request queues behind the same per-account lock and holds a database connection while it waits, so a few hundred parallel creates get slow and start failing where one batch call finishes in seconds. If you have more than a handful of contacts, send them here — and if you have more than 10,000, send sequential batches rather than parallel single creates.

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | Yes | The sending domain these contacts belong to (one of your domains, e.g. `yourdomain.com`). Can also be passed as a query parameter — `?domain=yourdomain.com` — which is what you want when the body is a bare JSON array. A missing `domain`, or one that is not yours, returns `422 validation_error`. |
| `contacts` | array | Yes | Array of contact objects to import. You can also send a bare JSON array as the body instead of wrapping it in `{ contacts: [...] }`. |
| `contacts[].email` | string | Yes | Email address. Rows with a missing or invalid email are skipped. |
| `contacts[].first_name` | string | No | Optional first name. |
| `contacts[].last_name` | string | No | Optional last name. |
| `contacts[].unsubscribed` | boolean | No | Optional opt-out flag. Defaults to false. |
| `contacts[].properties` | object | No | Optional map of custom property key/value pairs. Unlike `POST /contacts`, this endpoint does **not** check keys against your contact-property registry — any key matching `\w{1,64}` is accepted and stored (up to 50 keys per contact, values truncated at 1000 characters). Unregistered keys are written to the contact but have no declared type or fallback, so register them with `POST /contact-properties` if you want them usable as merge tags. |
| `on_conflict` | string | No | `upsert` (default) — update the existing contact when the email already exists. `skip` — leave the existing contact untouched. Can also be passed as a query parameter, `?on_conflict=skip`, for use with a bare JSON array body. |

**Node.js**

```js
const res = await fetch('https://www.sendping.co/api/contacts/batch', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer mb_xxxxxxxxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}),
});
const data = await res.json();
console.log(data);
```

**Ruby**

```ruby
require 'net/http'
require 'uri'

uri = URI('https://www.sendping.co/api/contacts/batch')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri)
req['Authorization'] = 'Bearer mb_xxxxxxxxx'
req['Content-Type'] = 'application/json'
req.body = <<~JSON
{
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}
JSON
res = http.request(req)
puts res.body
```

**PHP**

```php
<?php
$ch = curl_init('https://www.sendping.co/api/contacts/batch');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer mb_xxxxxxxxx',
        'User-Agent: my-app/1.0',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => <<<'JSON'
{
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}
JSON,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
```

**Python**

```python
import requests

res = requests.post(
    "https://www.sendping.co/api/contacts/batch",
    headers={"Authorization": "Bearer mb_xxxxxxxxx", "Content-Type": "application/json"},
    json={
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
},
)
print(res.json())
```

**Go**

```go
package main

import (
    "fmt"
    "io"
    "net/http"
    "strings"
)

func main() {
    req, _ := http.NewRequest("POST", "https://www.sendping.co/api/contacts/batch", strings.NewReader(`{
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}`))
    req.Header.Set("Authorization", "Bearer mb_xxxxxxxxx")
    req.Header.Set("Content-Type", "application/json")
    res, _ := http.DefaultClient.Do(req)
    defer res.Body.Close()
    out, _ := io.ReadAll(res.Body)
    fmt.Println(string(out))
}
```

**Rust**

```rust
use reqwest::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = Client::new()
        .post("https://www.sendping.co/api/contacts/batch")
        .header("Authorization", "Bearer mb_xxxxxxxxx")
        .header("User-Agent", "my-app/1.0")
        .header("Content-Type", "application/json")
        .body(r#"{
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}"#)
        .send()
        .await?;
    println!("{}", res.text().await?);
    Ok(())
}
```

**Java**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://www.sendping.co/api/contacts/batch"))
    .header("Authorization", "Bearer mb_xxxxxxxxx")
    .header("User-Agent", "my-app/1.0")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
{
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}
"""))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

**.NET**

```csharp
using System.Net.Http;
using System.Text;

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://www.sendping.co/api/contacts/batch");
request.Headers.Add("Authorization", "Bearer mb_xxxxxxxxx");
request.Headers.Add("User-Agent", "my-app/1.0");
request.Content = new StringContent(@"{
  ""domain"": ""yourdomain.com"",
  ""contacts"": [
    { ""email"": ""steve@example.com"", ""first_name"": ""Steve"", ""last_name"": ""Wozniak"" },
    { ""email"": ""ada@example.com"", ""first_name"": ""Ada"", ""last_name"": ""Lovelace"" }
  ]
}", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

**cURL**

```bash
curl -X POST 'https://www.sendping.co/api/contacts/batch' \
  -H 'Authorization: Bearer mb_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
  "domain": "yourdomain.com",
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
}'
```

### Response

```json
{
  "object": "contact_import",
  "imported": 1,
  "updated": 1,
  "skipped": 0,
  "total": 2
}
```

`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.

All plans support unlimited stored contacts. Each import is serialized and committed atomically. The JSON batch API accepts up to 10,000 rows per request; larger CSVs use the direct-S3 upload and preview flow.

## Audience-scoped variant

The nested route `POST /audiences/:audience_id/contacts/batch` imports the same JSON array into one specific audience instead of a domain — no `domain` field in the body. Everything else (the 10,000-row limit, `on_conflict`, the response, the contact-limit behaviour) is identical:

**Node.js**

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

const mb = new SendPing('mb_xxxxxxxxx');

const { data, error } = await mb.contacts.batch({
  audienceId: 'AUDIENCE_ID',
  "contacts": [
    { "email": "steve@example.com", "first_name": "Steve", "last_name": "Wozniak" },
    { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace" }
  ]
});
console.log({ data, error });
```

**Ruby**

```ruby
require "sendping"

SendPing.api_key = "mb_xxxxxxxxx"

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

**PHP**

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

use SendPing\SendPing;

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

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

**Python**

```python
import requests

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

**Go**

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

client := sendping.NewClient("mb_xxxxxxxxx")

imported, err := client.Contacts.Batch(&sendping.BatchContactsRequest{
    AudienceId: "AUDIENCE_ID",
    Contacts: []sendping.ContactInput{
        {
            Email:     "steve@example.com",
            FirstName: "Steve",
            LastName:  "Wozniak",
        },
        {Email: "ada@example.com", FirstName: "Ada", LastName: "Lovelace"},
    },
})
```

**Rust**

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

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

let items = vec![
    ContactInput::new("steve@example.com")
        .with_first_name("Steve")
        .with_last_name("Wozniak"),
    ContactInput::new("ada@example.com")
        .with_first_name("Ada")
        .with_last_name("Lovelace"),
];
let _imported = mb.contacts.batch("AUDIENCE_ID", items, None).await?;
```

**Java**

```java
import co.sendping.SendPing;
import co.sendping.SendPingResponse;
import co.sendping.requests.BatchContactsRequest;
import co.sendping.requests.ContactInput;

SendPing sendping = new SendPing("mb_xxxxxxxxx");

BatchContactsRequest request = BatchContactsRequest.builder()
        .audienceId("AUDIENCE_ID")
        .contact(ContactInput.builder()
                .email("steve@example.com")
                .firstName("Steve")
                .lastName("Wozniak")
                .build())
        .contact(ContactInput.builder()
                .email("ada@example.com")
                .firstName("Ada")
                .lastName("Lovelace")
                .build())
        .build();

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

**.NET**

```csharp
using SendPing;

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

var resp = await sendping.ContactBatchAsync("AUDIENCE_ID", new List<ContactInput>
{
    new()
    {
        Email = "steve@example.com",
        FirstName = "Steve",
        LastName = "Wozniak",
    },
    new()
    {
        Email = "ada@example.com",
        FirstName = "Ada",
        LastName = "Lovelace",
    },
});
```

**cURL**

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

An empty or entirely-invalid contacts array returns `422 validation_error`. More than 10,000 rows per request is rejected with `422 validation_error`. On the domain-scoped route a missing `domain`, a `domain` that is not one of yours, or an `audience_id` in the body returns `422 validation_error`; on the audience-scoped route an unknown audience returns `404 not_found`. See [Errors](https://www.sendping.co/docs/api/errors).
