When your agent reads Zendesk tickets and drafts customer responses, MultiMail adds sender identity, approval gates, and a tamper-evident audit trail before anything reaches a customer inbox.
Zendesk AI workflows—whether built with Zendesk AI Agents, custom apps via the REST API, or external orchestration—frequently end in an outbound email. The platform's native send path is built for human agents: it assumes the reply was reviewed before the send button was pressed. When an AI agent is doing that work autonomously, that assumption breaks.
MultiMail sits between your agent logic and the customer inbox. Your agent reads a Zendesk ticket, calls MultiMail's `send_email` endpoint to compose the reply, and depending on your oversight mode that message either routes to a human approver queue or goes out under a verified sender identity with a full audit record. The Zendesk ticket and the outbound email stay linked through a shared thread reference.
This pattern is useful whenever you need to satisfy CAN-SPAM sender identification requirements, log outbound AI correspondence for GDPR data subject request responses, or give a support manager a circuit breaker before an AI-drafted escalation reaches a high-value customer.
Zendesk's native send path uses your support domain, but it does not cryptographically bind the sending agent's identity to the message. MultiMail signs every outbound message with DKIM and records the agent credential that authorized the send, giving you an auditable chain from ticket to delivery receipt.
Set `oversight_mode: gated_send` and every AI-drafted reply surfaces in MultiMail's pending queue before delivery. Your support lead approves or rejects from a webhook-driven UI or the `list_pending` and `decide_email` API calls, without needing to touch Zendesk at all.
MultiMail enforces List-Unsubscribe headers, suppression lists, and opt-out processing on every outbound message. When a customer replies with an unsubscribe request, the `check_inbox` webhook fires and your agent can tag the Zendesk ticket accordingly before MultiMail blocks further sends to that address.
Pass a Zendesk ticket ID as the `thread_id` on your first `send_email` call. Subsequent `reply_email` calls on that thread preserve In-Reply-To and References headers, so the customer's email client threads the conversation correctly even when replies come from different agents or oversight modes.
Every `send_email`, `decide_email`, and `cancel_message` call is recorded with the agent credential, timestamp, and approval chain. For regulated industries—financial services under FINRA, healthcare under HIPAA—this log is exportable and maps directly to electronic communications retention requirements.
import httpx
ZENDESK_SUBDOMAIN = "acme"
ZENDESK_TOKEN = "your_zendesk_api_token"
ZENDESK_EMAIL = "[email protected]"
MULTIMAIL_TOKEN = "mm_live_your_token_here"
MULTIMAIL_BASE = "https://api.multimail.dev"
def handle_ticket(ticket_id: int) -> dict:
zd = httpx.Client(
base_url=f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2",
auth=(f"{ZENDESK_EMAIL}/token", ZENDESK_TOKEN),
)
ticket = zd.get(f"/tickets/{ticket_id}.json").json()["ticket"]
requester = zd.get(f"/users/{ticket[&"cm">#039;requester_id']}.json").json()["user"]
mm = httpx.Client(
base_url=MULTIMAIL_BASE,
headers={"Authorization": f"Bearer {MULTIMAIL_TOKEN}"},
)
response = mm.post("/send_email", json={
"from": "[email protected]",
"to": requester["email"],
"subject": f"Re: {ticket[&"cm">#039;subject']}",
"body": (
f"Hi {requester[&"cm">#039;name'].split()[0]},\n\n"
"Thank you for reaching out. Our team has reviewed your request "
"and will follow up within one business day.\n\n"
"Reference: "cm">#{ticket_id}"
),
"thread_id": f"zd-{ticket_id}",
"oversight_mode": "gated_send",
"metadata": {
"zendesk_ticket_id": ticket_id,
"zendesk_status": ticket["status"],
},
})
return response.json()
Fetch an open ticket from the Zendesk API, compose a reply using the ticket subject and requester email, and submit it to MultiMail under gated_send oversight so a human approves before delivery.
from flask import Flask, request, jsonify
import httpx
app = Flask(__name__)
MULTIMAIL_TOKEN = "mm_live_your_token_here"
ZENDESK_TOKEN = "your_zendesk_api_token"
ZENDESK_EMAIL = "[email protected]"
ZENDESK_SUBDOMAIN = "acme"
@app.post("/webhooks/multimail")
def multimail_webhook():
event = request.json
if event.get("type") != "email.pending_approval":
return jsonify({"ok": True})
message_id = event["message_id"]
ticket_id = event.get("metadata", {}).get("zendesk_ticket_id")
decision = "reject"
if ticket_id:
zd = httpx.Client(
base_url=f"https://{ZENDESK_SUBDOMAIN}.zendesk.com/api/v2",
auth=(f"{ZENDESK_EMAIL}/token", ZENDESK_TOKEN),
)
ticket = zd.get(f"/tickets/{ticket_id}.json").json()["ticket"]
tags = ticket.get("tags", [])
if "low-risk" in tags and ticket["priority"] in ("low", "normal"):
decision = "approve"
mm = httpx.Client(
base_url="https://api.multimail.dev",
headers={"Authorization": f"Bearer {MULTIMAIL_TOKEN}"},
)
mm.post("/decide_email", json={
"message_id": message_id,
"decision": decision,
})
return jsonify({"ok": True})
MultiMail fires a webhook when an email enters the approval queue. This handler receives the event, looks up the associated Zendesk ticket, and auto-approves replies for tickets tagged 'low-risk' while routing others to the support queue.
import { serve } from "@hono/node-server";
import { Hono } from "hono";
const app = new Hono();
const ZENDESK_SUBDOMAIN = process.env.ZENDESK_SUBDOMAIN;
const ZENDESK_EMAIL = process.env.ZENDESK_EMAIL;
const ZENDESK_TOKEN = process.env.ZENDESK_TOKEN;
const MULTIMAIL_TOKEN = process.env.MULTIMAIL_TOKEN;
app.post("/webhooks/inbound", async (c) => {
const event = await c.req.json();
if (event.type !== "email.received") {
return c.json({ ok: true });
}
const { from, subject, body_text, thread_id } = event;
"cm">// thread_id was set as zd-{ticket_id} on the original send
const ticketId = thread_id?.replace(/^zd-/, "");
if (!ticketId) {
return c.json({ ok: true });
}
const zdBase = `https:"cm">//${ZENDESK_SUBDOMAIN}.zendesk.com/api/v2`;
const zdAuth = Buffer.from(`${ZENDESK_EMAIL}/token:${ZENDESK_TOKEN}`).toString("base64");
await fetch(`${zdBase}/tickets/${ticketId}.json`, {
method: "PUT",
headers: {
Authorization: `Basic ${zdAuth}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
ticket: {
comment: {
body: `Customer replied via email:\n\n${body_text}`,
public: false,
author_id: null,
},
status: "open",
},
}),
});
"cm">// Tag the MultiMail message so it's searchable by ticket
await fetch("https://api.multimail.dev/tag_email", {
method: "POST",
headers: {
Authorization: `Bearer ${MULTIMAIL_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message_id: event.message_id,
tags: [`zendesk:${ticketId}`, "customer-reply"],
}),
});
return c.json({ ok: true });
});
serve({ fetch: app.fetch, port: 3000 });
Configure MultiMail to forward inbound emails on your support mailbox to a webhook. This handler parses the message, finds the linked Zendesk ticket by thread reference, and adds a comment so agents stay in sync.
import httpx
MULTIMAIL_TOKEN = "mm_live_your_token_here"
def get_pending_zendesk_replies() -> list[dict]:
mm = httpx.Client(
base_url="https://api.multimail.dev",
headers={"Authorization": f"Bearer {MULTIMAIL_TOKEN}"},
)
result = mm.get("/list_pending").json()
pending = result.get("messages", [])
"cm"># Filter to messages that originated from Zendesk tickets
zendesk_pending = [
msg for msg in pending
if "zendesk_ticket_id" in msg.get("metadata", {})
]
for msg in zendesk_pending:
ticket_id = msg["metadata"]["zendesk_ticket_id"]
print(f"[{msg[&"cm">#039;message_id']}] Ticket #{ticket_id}")
print(f" To: {msg[&"cm">#039;to']}")
print(f" Subject: {msg[&"cm">#039;subject']}")
print(f" Created: {msg[&"cm">#039;created_at']}")
print()
return zendesk_pending
if __name__ == "__main__":
pending = get_pending_zendesk_replies()
print(f"{len(pending)} Zendesk replies awaiting approval")
Before a support shift ends, query MultiMail for all messages awaiting approval that originated from Zendesk tickets, and surface them for batch review.
Sign up at multimail.dev, create a mailbox (e.g. `[email protected]` or a custom domain), and copy your `mm_live_...` API key from the dashboard. Set `oversight_mode` to `gated_send` while you test—this queues all sends for manual approval so nothing goes to customers accidentally.
curl -X POST https://api.multimail.dev/create_mailbox \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{"address": "[email protected]", "display_name": "Acme Support"}'In your Zendesk admin panel, create an API token under Admin > Apps and Integrations > Zendesk API. Store both the Zendesk token and your MultiMail token as environment variables. Your integration will authenticate to both APIs independently.
export ZENDESK_SUBDOMAIN=acme
export [email protected]
export ZENDESK_TOKEN=your_zendesk_api_token
export MULTIMAIL_TOKEN=mm_live_your_token_herePoint your MultiMail mailbox at the webhook endpoint your server exposes. MultiMail will POST `email.received` events here whenever a customer replies. Also register for `email.pending_approval` events to power your approval flow.
curl -X POST https://api.multimail.dev/webhooks \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{
"url": "https://your-server.example.com/webhooks/inbound",
"events": ["email.received", "email.pending_approval", "email.delivered"],
"mailbox": "[email protected]"
}&"cm">#039;Use the Python or JavaScript examples above to read a Zendesk ticket and call MultiMail's `send_email` endpoint. Set `thread_id` to `zd-{ticket_id}` on every send so inbound replies can be routed back to the right ticket.
"cm"># Quick smoke test: send a gated reply for ticket #12345
python -c "
import httpx
mm = httpx.Client(base_url=&"cm">#039;https://api.multimail.dev', headers={'Authorization': 'Bearer $MULTIMAIL_API_KEY'})
r = mm.post(&"cm">#039;/send_email', json={
&"cm">#039;from': '[email protected]',
&"cm">#039;to': '[email protected]',
&"cm">#039;subject': 'Re: Your request',
&"cm">#039;body': 'Thanks for reaching out. We are looking into this.',
&"cm">#039;thread_id': 'zd-12345',
&"cm">#039;oversight_mode': 'gated_send',
&"cm">#039;metadata': {'zendesk_ticket_id': 12345}
})
print(r.json())
"Call `list_pending` to confirm your test message appeared in the queue, then call `decide_email` with `approve` to release it. Once the end-to-end flow works under `gated_send`, you can move high-confidence ticket categories to `monitored` oversight and reserve `gated_send` for escalations and first-contact replies.
"cm"># List pending, then approve the first one
curl https://api.multimail.dev/list_pending \
-H "Authorization: Bearer $MULTIMAIL_API_KEY"
"cm"># Approve by message_id returned above
curl -X POST https://api.multimail.dev/decide_email \
-H "Authorization: Bearer $MULTIMAIL_API_KEY" \
-H "Content-Type: application/json" \
-d &"cm">#039;{"message_id": "msg_abc123", "decision": "approve"}'Email infrastructure built for AI agents. Verifiable identity, graduated oversight, and a 38-tool MCP server. Formally verified in Lean 4.