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",
status: "subscribed",
tags: ["beta", "newsletter"],
source: "signup-form",
consent: { source: "checkout", ip: "203.0.113.4", userAgent: "Mozilla/5.0 …" },
});

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 an 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

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