Skip to content
Guides

Contacts & audiences

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/audiencesPermission: contacts:read | contacts:write | events:write
GET/api/v1/audiences/{id}Permission: contacts:read | contacts:write | events:write
TypeScript
const { data } = await mm.audiences.list();
for (const audience of data?.data ?? []) {  console.log(audience.id, audience.name, audience.stats.subscribed);}
Audience objectJSON
{  "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}/contactsPermission: contacts:write
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);

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.

emailstringRequired
Stored lowercased and trimmed.
first_name, last_namestring | null
Up to 100 characters each.
phonestring | null
Free-form, up to 50 characters.
birthdaystring | null
"MM-DD". "YYYY-MM-DD" is accepted and the year dropped, like Mailchimp.
fieldsobject
Custom field values by key, e.g. { "COMPANY": "Acme" }.
tagsstring[]
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_sourcestring
Where and how they agreed, e.g. "Checkout checkbox". Saved as their consent record.
ipstring
Their IP address when they opted in, for the consent record.
skip_double_opt_inboolean
Full-access keys only, and only with consent_source: for people who already confirmed elsewhere.
update_existingboolean
Default true: overwrite the fields you send. false: only fill blanks.
Response201 CreatedJSON
{  "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#

subscribedyes
Opted in (and confirmed, on double opt-in audiences).
pendingno
Waiting for them to click the confirmation email.
unsubscribedno
Opted out. Only they can opt back in.
bouncedno
Their address hard-bounced. Suppressed for good.
complainedno
They marked an email as spam. Suppressed for good.
archivedno
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.

// 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" });

List contacts#

GET/api/v1/audiences/{id}/contactsPermission: 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.

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);

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.

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" });

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.

// Archive: hidden, never emailed, doesn't count toward your contact limitawait mm.contacts.remove({ audienceId, email: "sam@example.com" });
// Erase (GDPR): deletes their data; the address stays suppressed so it's never re-mailedawait mm.contacts.remove({ audienceId, email: "sam@example.com", permanent: true });

Next: label contacts with tags, or start an automation with an event.