AI-Driven Email from HubSpot CRM Data

Pull contact and deal context from HubSpot, generate personalized emails with your AI agent, and deliver through MultiMail with recipient-level oversight and full audit trails.


HubSpot stores the contact lifecycle data your AI agents need to personalize outbound email: deal stage, last activity, lifecycle stage, company size, and custom properties. The gap is execution — HubSpot's native automation runs static sequences, not dynamic agent-generated content tailored to each contact in real time.

MultiMail fills that gap. Your agent reads CRM context from HubSpot, composes email via the MultiMail API, and sends through managed mailboxes with full oversight controls. Every send is logged, every approval request is auditable, and every bounce or reply flows back as a webhook you can act on.

This pattern is particularly useful for outbound prospecting, post-demo follow-up, and renewal sequences where the right message depends on context that only exists at send time — not when you built the workflow template.

Built for HubSpot developers

Recipient-level approval queues

HubSpot workflows can trigger thousands of contacts simultaneously. MultiMail's gated_send mode queues each AI-generated email for human review before delivery, so a bad prompt or stale CRM data doesn't produce a mass send error.

Audit trail independent of HubSpot

MultiMail logs every send attempt, approval decision, delivery status, and reply event against the mailbox — not inside HubSpot's activity feed. This matters for SOC 2 compliance and post-incident review when you need to reconstruct exactly what your agent sent and when.

CAN-SPAM and GDPR compliance controls

MultiMail enforces unsubscribe handling at the API layer. Agents cannot send to contacts who have opted out, and every outbound message includes required headers. This runs independently of HubSpot's suppression lists so you have defense in depth.

Inbound reply routing back to your agent

When a prospect replies to an agent-sent email, MultiMail fires a webhook to your backend. Your agent can read the reply via check_inbox or read_email, update the HubSpot contact via their API, and decide whether to respond or escalate — closing the loop without manual handoff.

Oversight mode tuned per audience segment

Run gated_all for cold outbound where every message needs review, monitored for warm pipeline follow-ups, and autonomous for transactional notifications like meeting confirmations. Different HubSpot lists can map to different MultiMail oversight modes.


Get started in minutes

Fetch HubSpot contact and send via MultiMail
python
import httpx

HUBSPOT_TOKEN = "pat-na1-..."
MULTIMAIL_TOKEN = "mm_live_..."

def get_hubspot_contact(contact_id: str) -> dict:
    r = httpx.get(
        f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
        params={"properties": "firstname,lastname,email,lifecyclestage,hs_lead_status"},
        headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
    )
    r.raise_for_status()
    return r.json()["properties"]

def send_followup(contact_id: str, composed_body: str):
    contact = get_hubspot_contact(contact_id)
    recipient = f"{contact[&"cm">#039;firstname']} {contact['lastname']} <{contact['email']}>"

    r = httpx.post(
        "https://api.multimail.dev/v1/send_email",
        headers={"Authorization": f"Bearer {MULTIMAIL_TOKEN}"},
        json={
            "from": "[email protected]",
            "to": recipient,
            "subject": f"Following up, {contact[&"cm">#039;firstname']}",
            "body": composed_body,
            "oversight_mode": "gated_send",
            "metadata": {
                "hubspot_contact_id": contact_id,
                "lifecycle_stage": contact.get("lifecyclestage"),
            },
        },
    )
    r.raise_for_status()
    return r.json()  # {"message_id": "msg_...", "status": "pending_approval"}

Pull deal and contact properties from HubSpot's CRM API, then post a personalized email through MultiMail's send_email endpoint.

Process HubSpot webhook trigger and queue emails
python
from fastapi import FastAPI, Request
import httpx

app = FastAPI()
HUBSPOT_TOKEN = "pat-na1-..."
MM_TOKEN = "mm_live_..."

@app.post("/webhooks/hubspot/deal-stage")
async def handle_deal_stage(request: Request):
    payload = await request.json()
    "cm"># HubSpot sends array of subscription events
    for event in payload:
        if event.get("propertyName") != "dealstage":
            continue
        deal_id = event["objectId"]
        new_stage = event["propertyValue"]

        "cm"># Fetch associated contacts from HubSpot
        assoc = httpx.get(
            f"https://api.hubapi.com/crm/v3/objects/deals/{deal_id}/associations/contacts",
            headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
        ).json()

        for contact_ref in assoc.get("results", []):
            contact_id = contact_ref["id"]
            contact = httpx.get(
                f"https://api.hubapi.com/crm/v3/objects/contacts/{contact_id}",
                params={"properties": "firstname,email"},
                headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
            ).json()["properties"]

            body = generate_stage_email(contact["firstname"], new_stage)  "cm"># your LLM call

            httpx.post(
                "https://api.multimail.dev/v1/send_email",
                headers={"Authorization": f"Bearer {MM_TOKEN}"},
                json={
                    "from": "[email protected]",
                    "to": contact["email"],
                    "subject": f"Update on your proposal, {contact[&"cm">#039;firstname']}",
                    "body": body,
                    "oversight_mode": "gated_send",
                    "metadata": {"deal_id": str(deal_id), "stage": new_stage},
                },
            )

    return {"queued": True}

Receive a HubSpot workflow webhook when a deal moves to a new stage, generate emails for all associated contacts, and submit them to MultiMail's approval queue.

Handle MultiMail reply webhook and update HubSpot
python
from fastapi import FastAPI, Request
import httpx

app = FastAPI()
HUBSPOT_TOKEN = "pat-na1-..."
MM_TOKEN = "mm_live_..."

@app.post("/webhooks/multimail/reply")
async def handle_reply(request: Request):
    event = await request.json()
    if event.get("type") != "email.received":
        return {"ok": True}

    message_id = event["data"]["in_reply_to_message_id"]
    mailbox = event["data"]["mailbox"]
    from_address = event["data"]["from"]

    "cm"># Read the full reply via MultiMail
    reply = httpx.get(
        f"https://api.multimail.dev/v1/read_email",
        headers={"Authorization": f"Bearer {MM_TOKEN}"},
        params={"message_id": event["data"]["message_id"]},
    ).json()

    "cm"># Look up the HubSpot contact by email
    search = httpx.post(
        "https://api.hubapi.com/crm/v3/objects/contacts/search",
        headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
        json={
            "filterGroups": [{"filters": [{"propertyName": "email", "operator": "EQ", "value": from_address}]}],
            "properties": ["hs_object_id", "firstname"],
        },
    ).json()

    if not search.get("results"):
        return {"ok": True}

    contact_id = search["results"][0]["id"]

    "cm"># Create a HubSpot task for the rep to review the reply
    httpx.post(
        "https://api.hubapi.com/crm/v3/objects/tasks",
        headers={"Authorization": f"Bearer {HUBSPOT_TOKEN}"},
        json={
            "properties": {
                "hs_task_subject": f"Reply received — {from_address}",
                "hs_task_body": reply["body"][:500],
                "hs_task_status": "NOT_STARTED",
                "hs_task_type": "EMAIL",
                "hubspot_owner_id": "YOUR_OWNER_ID",
            },
            "associations": [{"to": {"id": contact_id}, "types": [{"associationCategory": "HUBSPOT_DEFINED", "associationTypeId": 204}]}],
        },
    )

    return {"ok": True}

When a prospect replies to an agent email, MultiMail fires a webhook. This handler reads the reply, updates the HubSpot contact's last activity, and creates a follow-up task.

List pending approvals and approve via MultiMail API
python
import httpx

MM_TOKEN = "mm_live_..."

def review_queue():
    pending = httpx.get(
        "https://api.multimail.dev/v1/list_pending",
        headers={"Authorization": f"Bearer {MM_TOKEN}"},
    ).json()

    for item in pending.get("messages", []):
        msg_id = item["message_id"]
        to = item["to"]
        subject = item["subject"]
        meta = item.get("metadata", {})
        deal_id = meta.get("deal_id", "unknown")

        print(f"[{msg_id}] To: {to} | Subject: {subject} | Deal: {deal_id}")
        decision = input("Approve? [y/n/skip]: ").strip().lower()

        if decision == "y":
            httpx.post(
                "https://api.multimail.dev/v1/decide_email",
                headers={"Authorization": f"Bearer {MM_TOKEN}"},
                json={"message_id": msg_id, "decision": "approve"},
            )
            print(f"  → Approved")
        elif decision == "n":
            httpx.post(
                "https://api.multimail.dev/v1/cancel_message",
                headers={"Authorization": f"Bearer {MM_TOKEN}"},
                json={"message_id": msg_id},
            )
            print(f"  → Cancelled")

if __name__ == "__main__":
    review_queue()

Pull all queued emails from MultiMail's approval queue, display them with HubSpot context, and approve or cancel individually.


Step by step

1

Create a MultiMail mailbox

Sign up at multimail.dev and create a mailbox for outbound use. You can use a @multimail.dev subdomain immediately or add a custom domain via DNS records in your account settings.

bash
curl -X POST https://api.multimail.dev/v1/create_mailbox \
  -H "Authorization: Bearer $MULTIMAIL_API_KEY..." \
  -H "Content-Type: application/json" \
  -d &"cm">#039;{"address": "[email protected]", "display_name": "Acme Sales"}'
2

Create a HubSpot private app and get your access token

In HubSpot, go to Settings → Integrations → Private Apps and create an app with scopes: crm.objects.contacts.read, crm.objects.deals.read, crm.objects.tasks.write. Copy the generated access token — this is your HUBSPOT_TOKEN for CRM API calls.

3

Set up a HubSpot workflow to trigger your agent

In HubSpot Workflows, create an enrollment trigger (e.g., deal stage changes to 'Proposal Sent') and add a webhook action pointing to your backend endpoint. The webhook payload will include the object ID your agent uses to fetch CRM context.

bash
"cm"># Your webhook handler endpoint — receives HubSpot events
"cm"># POST /webhooks/hubspot/deal-stage
"cm"># Body: [{"objectId": "12345", "propertyName": "dealstage", "propertyValue": "presentationscheduled"}]
4

Wire up MultiMail reply webhooks

In your MultiMail account settings, add a webhook endpoint for the email.received event type. Point it to your backend handler that reads the reply and updates the HubSpot contact record.

bash
"cm"># MultiMail webhook payload for inbound reply
"cm"># {
"cm">#   "type": "email.received",
"cm">#   "data": {
"cm">#     "message_id": "msg_recv_...",
"cm">#     "in_reply_to_message_id": "msg_...",
"cm">#     "from": "[email protected]",
"cm">#     "mailbox": "[email protected]"
"cm">#   }
"cm"># }
5

Choose oversight mode per workflow

Set oversight_mode in each send_email call based on the risk profile of the workflow. Cold outbound sequences should use gated_send or gated_all. Warm follow-ups after a demo can use monitored. Transactional confirmations (meeting links, document receipts) can use autonomous.


Common questions

Can I use this without a custom domain?
Yes. MultiMail provides @multimail.dev subdomains immediately. For production outbound, a custom domain improves deliverability — HubSpot contacts are more likely to engage with email from your company domain. You can add a custom domain in MultiMail account settings by adding the required DNS records.
How do I avoid sending to HubSpot contacts who have unsubscribed?
MultiMail enforces CAN-SPAM and GDPR opt-out handling at the API layer. You should also query HubSpot's unsubscribed status before sending: fetch the contact's unsubscribedemail property and skip any contact where it is true. Defense in depth — both systems check.
What happens if my agent sends an email that gets flagged as spam?
MultiMail fires a delivery status webhook with the bounce or spam classification. Your agent can read the event type and update the HubSpot contact's hs_lead_status to indicate the deliverability issue. Future sends to that contact should be paused until the issue is resolved.
Can multiple agents share the same mailbox?
Yes, but you'll lose per-agent attribution in the audit log. It's better to create one mailbox per workflow type (e.g., outbound@, followup@, renewals@) so you can filter MultiMail logs by mailbox to isolate which agent or workflow produced a given send.
How do I handle rate limits on the HubSpot API?
HubSpot's v3 CRM API allows 110 requests per 10 seconds on most plans. If your agent processes large HubSpot lists, batch contact lookups using the POST /crm/v3/objects/contacts/batch/read endpoint, which fetches up to 100 contacts per call. Cache the results in your backend rather than re-fetching on every send.
Does this replace HubSpot's native email sequences?
No — it complements them. HubSpot sequences are better for static, pre-written cadences where marketing controls the copy. MultiMail is better when the email content must be generated dynamically based on context that only exists at send time, or when you need per-message human approval that HubSpot's workflow engine doesn't support natively.

Explore more

The only agent email with a verifiable sender

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