# Contacts & audiences

> Add, update, unsubscribe and remove contacts with a Mailchimp-style API. Double opt-in and consent built in.

Source: https://www.mailmanny.com/docs/contacts
All docs for AI agents: https://www.mailmanny.com/llms.txt

An audience is a list of people who agreed to hear from you. The contacts API works like Mailchimp's: add someone by email and Mailmanny creates or updates them, applying double opt-in, your custom fields and tags.

## Audiences

`GET /api/v1/audiences` (permission: `contacts:read | contacts:write | events:write`)

`GET /api/v1/audiences/{id}` (permission: `contacts:read | contacts:write | events:write`)

```ts
const { data } = await mm.audiences.list();

for (const audience of data?.data ?? []) {
  console.log(audience.id, audience.name, audience.stats.subscribed);
}
```

**Audience object**

```json
{
  "object": "audience",
  "id": "0199e9c1-2f3a-7b4c-8d5e-6f7a8b9c0d1e",
  "name": "Bakery Newsletter",
  "double_opt_in": true,
  "from_name": "Manny's Bakery",
  "from_email": "hello@mannysbakery.com",
  "stats": { "subscribed": 1204, "pending": 37, "unsubscribed": 58, "total": 1311 },
  "fields": [
    { "key": "COMPANY", "label": "Company", "type": "text", "required": false, "options": [] },
    { "key": "FAV_BREAD", "label": "Favorite bread", "type": "dropdown", "required": false, "options": ["Sourdough", "Rye"] }
  ],
  "created_at": "2026-09-01T10:00:00.000Z"
}
```

`fields` lists the custom fields you can set on contacts, by `key`. Create fields in the app under Audience → Fields; the API rejects keys that don't exist, so typos don't pile up silently.

## Add or update a contact

`POST /api/v1/audiences/{id}/contacts` (permission: `contacts:write`)

**Node.js SDK**

```ts
const { data, error } = await mm.contacts.create({
  audienceId: "0199e9c1-2f3a-7b4c-8d5e-6f7a8b9c0d1e",
  email: "sam@example.com",
  firstName: "Sam",
  tags: ["newsletter"],
  consentSource: "Footer signup form on mannysbakery.com",
});

// Double opt-in audiences: "pending" until they click
// the confirmation email.
console.log(data?.status);
```

**curl**

```bash
curl -X POST https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts \
  -H "Authorization: Bearer $MAILMANNY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "sam@example.com",
    "first_name": "Sam",
    "tags": ["newsletter"],
    "consent_source": "Footer signup form on mannysbakery.com"
  }'
```

Emails are matched case-insensitively. The response is the contact plus `created`, with status 201 for a new contact and 200 for an existing one.

- `email` (string, required): Stored lowercased and trimmed.
- `first_name, last_name` (string | null): Up to 100 characters each.
- `phone` (string | null): Free-form, up to 50 characters.
- `birthday` (string | null): "MM-DD". "YYYY-MM-DD" is accepted and the year dropped, like Mailchimp.
- `fields` (object): Custom field values by key, e.g. { "COMPANY": "Acme" }.
- `tags` (string[]): Tag names to add. New names are created.
- `status` ("subscribed" | "pending"): Default "subscribed". On a double opt-in audience a new contact starts as "pending" and gets a confirmation email.
- `consent_source` (string): Where and how they agreed, e.g. "Checkout checkbox". Saved as their consent record.
- `ip` (string): Their IP address when they opted in, for the consent record.
- `skip_double_opt_in` (boolean): Full-access keys only, and only with consent_source: for people who already confirmed elsewhere.
- `update_existing` (boolean): Default true: overwrite the fields you send. false: only fill blanks.

> **Warning:** Nobody gets resubscribed by the API
> If a contact unsubscribed, bounced or marked you as spam, adding them again updates their profile but keeps their status. They can only come back by signing up again themselves. That protects your sending reputation, and honoring opt-outs is required by CAN-SPAM and GDPR.

> **Note:** Disposable addresses and the Free plan
> Throwaway inboxes (mailinator.com, 10minutemail.com and about 180,000 more) are never added: the request fails with 422 `validation_error` and `details.reason` `"disposable_email"`. On Free, adding a subscribed contact past 500 fails with 402 `plan_required`; contacts that start as pending (double opt-in) don't count until they confirm.

**201 Created**

```json
{
  "object": "contact",
  "id": "0199f0b2-6c1e-7a3d-9f2b-3c4d5e6f7a8b",
  "audience_id": "0199e9c1-2f3a-7b4c-8d5e-6f7a8b9c0d1e",
  "email": "sam@example.com",
  "status": "pending",
  "first_name": "Sam",
  "last_name": null,
  "phone": null,
  "birthday": "07-14",
  "fields": { "FAV_BREAD": "Sourdough" },
  "tags": ["newsletter"],
  "source": "api",
  "consent_source": "Footer signup form on mannysbakery.com",
  "consent_at": "2026-10-15T09:30:00.000Z",
  "opted_in_at": null,
  "unsubscribed_at": null,
  "rating": 2,
  "created_at": "2026-10-15T09:30:00.000Z",
  "updated_at": "2026-10-15T09:30:00.000Z",
  "created": true
}
```

### Contact statuses

- `subscribed` (yes): Opted in (and confirmed, on double opt-in audiences).
- `pending` (no): Waiting for them to click the confirmation email.
- `unsubscribed` (no): Opted out. Only they can opt back in.
- `bounced` (no): Their address hard-bounced. Suppressed for good.
- `complained` (no): They marked an email as spam. Suppressed for good.
- `archived` (no): Removed with DELETE. Kept for your records, never emailed.

## Get a contact

`GET /api/v1/audiences/{id}/contacts/{email_or_id}` (permission: `contacts:read`)

`{email_or_id}` takes a contact id, an email address, or a Mailchimp-style subscriber hash (the md5 of the lowercase email), so code written for Mailchimp ports over with few changes.

**Node.js SDK**

```ts
// By email, contact id, or Mailchimp subscriber hash (md5 of the lowercase email)
const { data: contact } = await mm.contacts.get({ audienceId, email: "sam@example.com" });
```

**curl**

```bash
curl https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts/sam@example.com \
  -H "Authorization: Bearer $MAILMANNY_API_KEY"
```

## List contacts

`GET /api/v1/audiences/{id}/contacts` (permission: `contacts:read`)

Newest first. Filter with `status`, page with `limit` (1–100, default 25) and `cursor`: pass the `next_cursor` of the previous page until `has_more` is false. Every list endpoint pages the same way.

**Node.js SDK**

```ts
let cursor: string | undefined;
do {
  const { data, error } = await mm.contacts.list({ audienceId, status: "subscribed", limit: 100, cursor });
  if (error) throw new Error(error.message);
  for (const contact of data.data) console.log(contact.email);
  cursor = data.next_cursor ?? undefined;
} while (cursor);
```

**curl**

```bash
curl "https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts?status=subscribed&limit=100" \
  -H "Authorization: Bearer $MAILMANNY_API_KEY"
```

## Update or unsubscribe

`PATCH /api/v1/audiences/{id}/contacts/{email_or_id}` (permission: `contacts:write`)

Send only the fields you want to change. `status` accepts `"unsubscribed"` only. Email addresses can't be changed: remove the contact and add the new address.

**Node.js SDK**

```ts
await mm.contacts.update({
  audienceId,
  email: "sam@example.com",
  lastName: "Rivera",
  fields: { FAV_BREAD: "Rye" },
});

// Unsubscribe (for your own preference center)
await mm.contacts.update({ audienceId, email: "sam@example.com", status: "unsubscribed" });
```

**curl**

```bash
curl -X PATCH https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts/sam@example.com \
  -H "Authorization: Bearer $MAILMANNY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "last_name": "Rivera", "fields": { "FAV_BREAD": "Rye" } }'
```

## Remove a contact

`DELETE /api/v1/audiences/{id}/contacts/{email_or_id}` (permission: `contacts:write`)

By default `DELETE` archives the contact, like Mailchimp. Add `?permanent=true` to erase their data for a GDPR request; erasing can't be undone, so it needs a full-access key. Either way they're never emailed again.

**Node.js SDK**

```ts
// Archive: hidden, never emailed, doesn't count toward your contact limit
await mm.contacts.remove({ audienceId, email: "sam@example.com" });

// Erase (GDPR): deletes their data; the address stays suppressed so it's never re-mailed
await mm.contacts.remove({ audienceId, email: "sam@example.com", permanent: true });
```

**curl**

```bash
curl -X DELETE "https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts/sam@example.com?permanent=true" \
  -H "Authorization: Bearer $MAILMANNY_API_KEY"
```

Next: label contacts with [tags](https://www.mailmanny.com/docs/tags.md), or start an automation with an [event](https://www.mailmanny.com/docs/events.md).
