# Managing contacts

> Add contacts to an audience with an email and optional name, track their subscribe state, and address them by id or email.

A **contact** is a single person on an audience. Contacts are nested under the audience that owns them — every contact operation runs against `/audiences/:audience_id/contacts`.

The only required field is `email`. You can optionally store a `first_name` and `last_name`, an `unsubscribed` flag that controls whether campaigns reach them, and a `properties` map of custom fields you can use to personalize campaigns.

## The contact object

```json
{
  "object": "contact",
  "id": "479e3145-dd0e-4f64-bf48-1d4b6d4cd8f6",
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false,
  "properties": {
    "company_name": "Acme Corp"
  },
  "created_at": "2026-06-23T17:30:11.000Z"
}
```

Emails are stored lowercased. `first_name` and `last_name` are `null` when not provided. `properties` is always present and includes every registered custom property, merged with fallback values.

## Adding a contact

Send the `email` (required) plus any optional fields to the audience’s contacts collection. Alongside `first_name`, `last_name`, and `unsubscribed`, you can include a `properties` object of custom field values — each key must already exist as a property on the audience and the value must match its type.

**Node.js**

```js
import { MailBlastr } from 'mailblastr';

const mb = new MailBlastr('mb_xxxxxxxxx');

const { data, error } = await mb.contacts.create({
  audienceId: 'AUDIENCE_ID',
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
});
console.log({ data, error });
```

**Ruby**

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

uri = URI('https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts')
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
{
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
}
JSON
res = http.request(req)
puts res.body
```

**PHP**

```php
<?php
$ch = curl_init('https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer mb_xxxxxxxxx',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => <<<'JSON'
{
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
}
JSON,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
```

**Python**

```python
import requests

res = requests.post(
    "https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts",
    headers={"Authorization": "Bearer mb_xxxxxxxxx", "Content-Type": "application/json"},
    json={
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
},
)
print(res.json())
```

**Go**

```go
package main

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

func main() {
    req, _ := http.NewRequest("POST", "https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts", strings.NewReader(`{
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
}`))
    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://api.mailblastr.com/audiences/AUDIENCE_ID/contacts")
        .header("Authorization", "Bearer mb_xxxxxxxxx")
        .header("Content-Type", "application/json")
        .body(r#"{
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
}"#)
        .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://api.mailblastr.com/audiences/AUDIENCE_ID/contacts"))
    .header("Authorization", "Bearer mb_xxxxxxxxx")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
{
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
}
"""))
    .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://api.mailblastr.com/audiences/AUDIENCE_ID/contacts");
request.Headers.Add("Authorization", "Bearer mb_xxxxxxxxx");
request.Content = new StringContent(@"{
  ""email"": ""steve@example.com"",
  ""first_name"": ""Steve"",
  ""last_name"": ""Wozniak"",
  ""unsubscribed"": false
}", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

**cURL**

```bash
curl -X POST 'https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts' \
  -H 'Authorization: Bearer mb_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
  "email": "steve@example.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "unsubscribed": false
}'
```

## Upsert by email

Contacts are **unique by email within an audience**. If you create a contact whose email already exists in that audience, MailBlastr **updates** the existing contact instead of creating a duplicate — `first_name`, `last_name`, and `unsubscribed` are overwritten with the values you send. This makes the create call safe to call repeatedly (for example, on every signup) without producing duplicates.

> **Note:** Because create upserts, omit a field at your own risk: a missing `first_name`/`last_name` is stored as `null` and an absent `unsubscribed` defaults to `false`, overwriting any previous value. Use [PATCH](https://www.mailblastr.com/docs/api/contacts-update) when you want to change only some fields.

## Addressing a contact by id or email

Anywhere a contact identifier appears in the path — get, update, delete — you can pass either the contact `id` or its `email`. MailBlastr detects which you provided. Both of these refer to the same contact:

```bash
GET https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts/479e3145-dd0e-4f64-bf48-1d4b6d4cd8f6
GET https://api.mailblastr.com/audiences/AUDIENCE_ID/contacts/steve@example.com
```

## Contact properties

Every contact has four **default properties** — `email`, `first_name`, `last_name`, and `unsubscribed`. You can also define **custom properties** on an audience to store additional information (for example `company_name` or `plan`) and use those values to personalize campaigns.

Set custom values by passing a `properties` map when you create or [update](https://www.mailblastr.com/docs/api/contacts-update) a contact:

```json
{
  "email": "steve@example.com",
  "first_name": "Steve",
  "properties": {
    "company_name": "Acme Corp",
    "plan": "pro"
  }
}
```

> **Warning:** A property key must already exist on the audience and the value must match its declared type. Unknown keys or mismatched types are rejected and the contact is not changed. Property keys are case-sensitive — `company_name` and `companyName` are different keys.

## Bulk import by CSV

To add many contacts at once, import a CSV (up to **5 MB / 10,000 contacts** per import). Map each CSV column to a contact field — `email`, `first_name`, `last_name`, `unsubscribed` — or to a custom property. The `email` column is always required. When a row matches an existing contact by email, the `on_conflict` setting decides the outcome:

| on_conflict | Behavior |
| --- | --- |
| `upsert` | Update the existing contact with the values from the CSV row. |
| `skip` | Leave the existing contact untouched and skip the row. |

## Automatic creation

> **Note:** Contacts are also created automatically when an [automation](https://www.mailblastr.com/docs/automations/overview) runs for an email address that does not yet exist on the audience, so you do not have to pre-add every recipient.

## Next steps

- [Create a contact via the API](https://www.mailblastr.com/docs/api/contacts-create).
- [Update a contact](https://www.mailblastr.com/docs/api/contacts-update) — change a name, a property, or the subscribe state.
- [Manage unsubscribed contacts](https://www.mailblastr.com/docs/audiences/unsubscribed).
