Skip to main content

Contacts

A contact is a person on one project's list, unique by email. Contacts anchor unsubscribe links, campaign audiences, sequence enrollments, and engagement history.

contacts.ts
import { mailroom } from "./client";

// Upsert is idempotent on (project, email): safe to replay for the same person.
const contacts = await mailroom.contacts.upsert([
{ email: "ada@example.com", tags: ["beta"], source: "signup-form" },
{ email: "grace@example.com" },
]);

console.log(contacts.map((c) => `${c.email} → ${c.status}`));

Upsert semantics​

upsert is keyed on (project, email):

  • New email → inserted.
  • Existing email → updated in place, keeping its id.
  • Accepts one object or an array (max 1000 per call).

That makes it safe to replay on every sign-up, profile edit, or sync tick — no "does this contact exist?" round-trip.

await mailroom.contacts.upsert({
email: "ada@example.com",
firstName: "Ada",
lastName: "Lovelace",
displayName: "Ada Lovelace",
status: "subscribed",
tags: ["beta", "newsletter"],
source: "signup-form",
consent: { source: "checkout", ip: "203.0.113.4", userAgent: "Mozilla/5.0 …" },
});

Names follow the same preservation rule as other optional profile fields. Omit one to leave an existing value alone, or send null to clear it. Templates can read contact.firstName, contact.lastName, contact.displayName, and contact.fullName. fullName uses the display name when set, otherwise it joins the available first and last names. Existing name aliases in custom attributes remain read-only fallbacks until a canonical value is set or explicitly cleared.

consent is recorded for compliance evidence — capture the real IP and user agent from the request that opted them in, not your server's.

Status​

StatusMeaning
subscribedMailable
unsubscribedOpted out; marketing sends are skipped
pendingAwaiting confirmation (double opt-in flows)
bouncedDelivery hard-failed; usually also suppressed

Status is yours to set on upsert, but let Mailroom own the transitions it detects: unsubscribes and bounces are applied automatically.

Preserving ids during a migration​

Coming from another system with unsubscribe links already in the wild? Pass your existing uuid as id — honored on insert only, so an existing contact keeps the id Mailroom already gave it:

await mailroom.contacts.upsert([
{ id: "3f1a…", email: "ada@example.com" }, // keeps old ?c=<id> links working
]);

Reading the list​

Three shapes, same filters (status, tag, q for a name or email substring, limit):

const page = await mailroom.contacts.list({ status: "subscribed" }); // one page
const cursorPage = await mailroom.contacts.listPage({ cursor }); // manual paging
for await (const c of mailroom.contacts.listAll({ tag: "beta" })) { } // every match

listPage also returns totalCount, nextCursor, and previousCursor. Pass the matching cursor with direction: "next" or direction: "previous" for a bidirectional contact table.

See Pagination for which to reach for.

Unsubscribing​

await mailroom.contacts.unsubscribe(contactId);
// or the alias: await mailroom.unsubscribe(contactId);

This is the same effect as the recipient clicking the link in an email: status flips to unsubscribed and a contact.unsubscribed webhook fires — mirror it into your own store from that event rather than assuming your call was the only source of truth.

There is no "delete contact" endpoint. Unsubscribing preserves the record, which is what keeps you from re-mailing someone who opted out.

Scopes​

contacts:read for the read methods, contacts:write for upsert and unsubscribe.

Reference​