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#
const { data } = await mm.audiences.list();
for (const audience of data?.data ?? []) { console.log(audience.id, audience.name, audience.stats.subscribed);}{ "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#
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 -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.
- 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.
{ "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#
{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" });curl https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts/sam@example.com \ -H "Authorization: Bearer $MAILMANNY_API_KEY"List contacts#
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);curl "https://www.mailmanny.com/api/v1/audiences/$AUDIENCE_ID/contacts?status=subscribed&limit=100" \ -H "Authorization: Bearer $MAILMANNY_API_KEY"Update or unsubscribe#
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" });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#
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 });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, or start an automation with an event.