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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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"}'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.
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.
"cm"># Your webhook handler endpoint — receives HubSpot events
"cm"># POST /webhooks/hubspot/deal-stage
"cm"># Body: [{"objectId": "12345", "propertyName": "dealstage", "propertyValue": "presentationscheduled"}]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.
"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"># }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.
Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.