Contacts
Team-global contacts with subscribe state and custom properties.
Contacts are team-global: one list per team, one row per email address
(case-insensitive unique). There is no "audiences" concept — a contact belongs
to your team directly, and you target subsets with
segments and topics. On MepMail
Cloud the Free plan holds up to 1,000 contacts (creating one past that returns
403 plan_limit_reached; existing contacts still update); paid plans have no
contact limit.
If you are migrating from Resend: the Resend SDK's contact methods work
against MepMail whenever audienceId is omitted — the contact paths are
the same, minus the audience nesting.
What a contact holds
email— the identity. Creating a second contact with the same email (any casing) returns409.first_name,last_nameunsubscribed— the global subscribe state. Unsubscribed contacts are excluded from every broadcast.properties— a flat map of custom string values (plan: "pro",city: "Berlin"). Nested objects and arrays are rejected with422. Properties feed template merge fields and segment filters.PATCH /contacts/{id}mergespropertieskey by key; anullvalue removes that key. The stored map holds at most 100 keys, none of them empty.
API
Contacts are managed via POST/GET/PATCH/DELETE /contacts and
GET /contacts/{id} — see the API reference. The {id}
path segment accepts either the contact UUID or its email address; email
matching is case-insensitive.
Two MepMail extensions make reading an audience cheap. GET /contacts
and GET /segments/{id}/contacts accept include=properties,topics and
attach to every item the {type, value} property map and the topic rows that
GET /contacts/{id} and GET /contacts/{id}/topics return; without include
the items keep the Resend shape. POST /contacts/batch/get reads up to 1,000
contacts by id or email in one request, in request order, with the same
include; entries that match no contact are listed under missing rather
than failing the call. One call is one request against the rate limit.
Topic subscriptions are set per contact with PATCH /contacts/{id}/topics,
and GET /contacts/{id}/topics reads them back with defaults applied: each
topic's effective subscription, whether it was chosen explicitly, and its
visibility (the hosted page shows public topics only).
POST /contacts/{id}/preferences-link mints the contact's preference-center
URL — { "object": "preferences_link", "contact": "<uuid>", "url": "..." } —
the same page the unsubscribe links in their emails open, so a settings screen
in your product can deep-link into it. The link has no expiry and lets its
holder change that contact's preferences, including the global unsubscribe, so
hand it only to the contact. Also available as the
create_contact_preferences_link MCP tool.
Every change to a contact publishes a
webhook event: contact.created,
contact.updated, contact.deleted, contact.unsubscribed,
contact.resubscribed, contact.topic_opt_in and contact.topic_opt_out,
each with the source that made the change.
Bulk delete
POST /contacts/batch/remove deletes up to 1,000 contacts in one request, by
ids or by emails (exactly one of the two; email matching is
case-insensitive), and returns the rows actually deleted — unknown entries are
skipped. Deleting keeps the contact's emails in the log, where they age out
with the team's retention window; erase: true also scrubs each address from
email history, event payloads and API logs, the same as
DELETE /contacts/{id}?erase=true. Resend has no bulk deletion; this is a
MepMail extension, also exposed as the delete_contacts MCP tool.
curl -X POST "https://api-mepmail.je4ndev.com/contacts/batch/remove" \
-H "Authorization: Bearer ms_..." \
-H "Content-Type: application/json" \
-d '{ "emails": ["[email protected]", "[email protected]"] }'Bulk create
POST /contacts/batch takes a JSON array of 1–1000 items, each shaped like
a POST /contacts body, and writes them in one transaction (a MepMail
extension — Resend imports contacts only via CSV). The on_conflict query
parameter decides what happens to an item whose email already belongs to a
contact, or repeats inside the batch:
error(default) — the item fails:409 Contact already existsfor an existing contact,422 Duplicate email in batchfor a repeat.skip— the existing contact (or the first occurrence) is left untouched and reported withstatus: "skipped"and its id.upsert— the item is merged into the existing contact:first_nameandlast_nameonly when provided,propertieskey by key (provided keys overwrite),segmentsadded,topicsupserted. Repeats collapse into one write.
A batch never re-subscribes anyone: unsubscribed: true opts the contact
out, but unsubscribed: false on an already unsubscribed contact is ignored
— that stays an explicit PATCH /contacts/{id}. The suppression list is
never touched either.
The x-batch-validation header picks strict (default — the first failing
item rejects the whole batch with its own status and a contacts.{index}:
message prefix, nothing written) or permissive (the valid subset is written
and failures are listed in errors).
curl -X POST "https://api-mepmail.je4ndev.com/contacts/batch?on_conflict=upsert" \
-H "Authorization: Bearer ms_..." \
-H "Content-Type: application/json" \
-H "x-batch-validation: permissive" \
-d '[
{ "email": "[email protected]", "first_name": "Ana", "properties": { "plan": "pro" } },
{ "email": "not-an-address" }
]'{
"data": [{ "object": "contact", "index": 0, "id": "9b2f…", "status": "updated" }],
"counts": { "created": 0, "updated": 1, "skipped": 0, "failed": 1 },
"errors": [{ "index": 1, "message": "email: Invalid email address" }]
}data keeps request order and lists one entry per successful item (status
created, updated or skipped); counts always sum to the request
length; errors appears only in permissive mode.
CSV import and export
The dashboard imports contacts from CSV (parsed client-side, then created in bulk) and exports the current list — including segment or topic filtered views — back to CSV.
Unsubscribes
Broadcast emails carry RFC 8058 one-click List-Unsubscribe headers and a
hosted unsubscribe page. A recipient can unsubscribe globally or opt out of
individual topics. Global unsubscribes set
unsubscribed: true on the contact and stop topic sends and broadcasts;
transactional sends without a topic_id still arrive — see
Suppressions.