Contact intelligence your agents can actually use

Store contact records, sync email interaction history, and attach custom metadata — so agents personalize outreach without building pipelines across five systems.


Why this matters

AI agents that send email often work blind. Contact data lives in CRMs, product databases, and support tickets — none of which the agent can query at send time without brittle integrations. The result: generic messages, missed context, and repeated asks for information the agent should already have. Scattered identity data also creates GDPR compliance risk: a right-to-erasure request requires coordinated deletes across every system that touched the contact.


How MultiMail solves this

MultiMail ties contact records directly to email activity. When an agent reads or sends a message, interaction history updates automatically. Custom metadata — plan tier, last feature used, open support tickets — travels with the contact record, so every agent that touches that mailbox has full context without building ETL pipelines. GDPR deletion propagates in a single API call.

1

Create or upsert contact records

POST a contact with email address, name, and any custom metadata fields. Existing records are updated on email match; new records are created on miss. Metadata is arbitrary JSON — no schema migration required.

2

Interaction history syncs automatically

Every send_email, reply_email, or inbound webhook event is appended to the matching contact's interaction log. Agents query this log to see what was sent, when, and whether the recipient replied — without touching your CRM.

3

Search contacts before acting

Use manage_contacts to find contacts by email, domain, metadata field, or interaction recency before composing a message. Agents avoid duplicate outreach and can branch logic based on prior thread context.

4

Personalize outreach from stored context

At send time, the agent reads the contact record to pull metadata — product tier, last login, open items — and constructs a message grounded in actual account state rather than templated assumptions.

5

Update contact state after each interaction

After a send or reply, the agent writes the new state back to the contact record: last contacted date, campaign stage, custom flags. Downstream agents pick up the updated state without re-fetching from external systems.


Implementation

Upsert a contact with custom metadata
python
import multimail

client = multimail.Client(api_key="mm_live_...")

contact = client.contacts.upsert(
    email="[email protected]",
    name="Elena Vasquez",
    metadata={
        "plan": "pro",
        "last_login": "2026-04-17",
        "open_tickets": 2,
        "product_interest": ["analytics", "export"]
    }
)

print(contact.id)  "cm"># contact_01HV...

Create or update a contact record with structured metadata your agents can read back at send time.

Search contacts before composing
python
import multimail

client = multimail.Client(api_key="mm_live_...")

"cm"># Find pro-plan contacts not reached in 30+ days
results = client.contacts.search(
    domain="acmecorp.com",
    filters={
        "last_contacted_before": "2026-03-20",
        "metadata.plan": "pro"
    },
    limit=50
)

for contact in results.items:
    print(contact.email, contact.metadata.get("last_login"))

Find contacts by domain and metadata field so the agent checks prior history before any outreach run.

Personalize and send from contact context
python
import multimail

client = multimail.Client(api_key="mm_live_...")

contact = client.contacts.get(email="[email protected]")
meta = contact.metadata
first_name = contact.name.split()[0]

if meta.get("open_tickets", 0) > 0:
    subject = "Following up on your open items"
    body = (
        f"Hi {first_name}, I noticed you have {meta[&"cm">#039;open_tickets']} open "
        f"support items. Based on your interest in "
        f"{&"cm">#039;, '.join(meta['product_interest'])}, here's the next best step..."
    )
else:
    subject = f"What&"cm">#039;s next for your {meta['plan']} account"
    body = f"Hi {first_name}, based on your recent activity..."

client.send_email(
    from_mailbox="[email protected]",
    to=contact.email,
    subject=subject,
    body=body,
    oversight_mode="monitored"
)

Read a contact record, build a message grounded in account state, and send via monitored oversight.

Update contact state after interaction
python
import multimail
from datetime import date

client = multimail.Client(api_key="mm_live_...")

client.contacts.update(
    email="[email protected]",
    metadata={
        "last_contacted": date.today().isoformat(),
        "campaign_stage": "follow_up_sent",
        "follow_up_due": "2026-04-26"
    }
)

Write the interaction outcome back to the contact record so the next agent has current state without re-fetching from upstream systems.

REST API: upsert contact
bash
curl -X POST https://api.multimail.dev/contacts \
  -H "Authorization: Bearer $MULTIMAIL_API_KEY..." \
  -H "Content-Type: application/json" \
  -d &"cm">#039;{
    "email": "[email protected]",
    "name": "Elena Vasquez",
    "upsert": true,
    "metadata": {
      "plan": "pro",
      "last_login": "2026-04-17",
      "product_interest": ["analytics", "export"]
    }
  }&"cm">#039;

Direct HTTP call to create or update a contact record from any runtime or language.

MCP: search contacts from Claude Desktop
text
Tool: manage_contacts
Input:
  query: "acmecorp.com"
  filters:
    metadata.plan: "pro"
    last_contacted_before: "2026-03-20"
  limit: 25

Returns: list of contact objects with email, name, metadata,
         last_contacted, interaction_count, and latest_thread_id

Call manage_contacts as an MCP tool from any MCP-compatible client to ground agent decisions in live contact state.


What you get

No schema migrations for metadata

Contact metadata is arbitrary JSON. Store product tier, account flags, or agent-specific state without defining schemas in advance. New keys are merged on update; existing keys are only overwritten if included in the payload.

Interaction history is automatic

Every email sent or received through MultiMail updates the relevant contact's interaction log. Agents query recency and thread count without parsing raw message headers or writing their own logging layer.

GDPR deletion propagates in one call

Deleting a contact cascades to associated interaction logs and metadata. Right-to-erasure requests require one API call rather than coordinated deletes across your CRM, data warehouse, and email provider.

Context survives agent handoffs

Because contact state lives in MultiMail rather than agent memory, a different agent or model version picks up the same context — no re-hydration prompts, no state reconstruction from raw email threads.

Pre-send deduplication via manage_contacts

Before any outreach batch, agents call manage_contacts to check recency and confirm no active thread exists with the contact. This eliminates duplicate sends without maintaining a separate suppression list.


Recommended oversight mode

Recommended
monitored
Contact management operations — creating records, updating metadata, syncing interaction state — are frequent, low-risk writes that don't benefit from per-action approval gates. Monitored mode lets agents operate at volume while emitting a notification stream you can audit. If your compliance posture requires pre-send review of the outreach messages themselves, layer gated_send on the send_email calls while leaving contact reads and writes on monitored.

Common questions

How do I handle GDPR right-to-erasure requests?
Call DELETE /contacts/{email} or client.contacts.delete(email=...) from the SDK. MultiMail cascades the deletion to all associated interaction logs and metadata. Stored message headers are scrubbed within 24 hours. You remain responsible for deleting any derived records in your own downstream systems.
Can I store arbitrary metadata fields, or is there a fixed schema?
Metadata is arbitrary JSON with no predefined schema. You can add, remove, or rename fields on any update call. New keys are merged into the existing record; existing keys are only overwritten if you include them in the update payload. Null values explicitly clear a field.
Does manage_contacts support filtering by metadata values?
Yes. Filter on scalar metadata fields using dot notation: metadata.plan=pro or metadata.open_tickets=2. Nested arrays are not indexed for equality filtering. For complex queries, retrieve a broader set and filter client-side.
How do agents avoid sending duplicate messages to the same contact?
Call manage_contacts before any outreach batch and check the last_contacted timestamp against your recency threshold. You can also call check_inbox on the agent mailbox to confirm no open thread exists with that contact before composing a new message.
Can I sync contacts from an existing CRM?
Yes. Use the upsert endpoint with upsert: true in a batch loop driven by your CRM export. Each call creates or updates the record on email address match. MultiMail does not have native CRM connectors — you write the sync script, MultiMail provides the storage and interaction tracking.
What happens to interaction history when I delete a mailbox?
Interaction logs reference the mailbox ID, not the address string. Deleting a mailbox orphans those log entries — they remain queryable on the contact record but are flagged as originating from a deleted mailbox. Delete contacts before mailboxes if you want clean removal.

Explore more use cases

The only agent email with a verifiable sender

Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 50-tool MCP server. Formally verified in Lean 4.