Stronger email guarantees for Zendesk AI workflows

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.

Built for Zendesk developers

Verified sender identity

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.

Approval gates before send

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.

CAN-SPAM and GDPR compliance handling

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.

Thread continuity across channels

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.

Tamper-evident audit log

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.


Get started in minutes

Read a Zendesk ticket and draft a reply via MultiMail
python
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.

Webhook handler: approve or reject pending replies
python
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.

Process inbound customer replies and update Zendesk tickets
javascript
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.

List and review pending approvals for a Zendesk ticket batch
python
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.


Step by step

1

Create a MultiMail API key and mailbox

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.

bash
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"}'
2

Configure Zendesk API credentials

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.

bash
export ZENDESK_SUBDOMAIN=acme
export [email protected]
export ZENDESK_TOKEN=your_zendesk_api_token
export MULTIMAIL_TOKEN=mm_live_your_token_here
3

Register a MultiMail inbound webhook

Point 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.

bash
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;
4

Wire up your first ticket-to-email handler

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.

bash
"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())
"
5

Verify the approval queue and go to production

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.

bash
"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"}'

Common questions

Does this replace Zendesk's native email channel?
No. Zendesk's native email channel is still the source of truth for ticket history, SLA tracking, and agent-facing interfaces. MultiMail handles only the outbound send path for AI-generated replies—it adds identity verification, approval gates, and compliance enforcement that Zendesk's send path does not provide for agent-authored content. Inbound customer replies can flow back to Zendesk as ticket comments via the webhook handler.
Can I use my existing Zendesk support domain instead of a multimail.dev address?
Yes. MultiMail supports custom domains. You add a DNS TXT record to verify ownership, and MultiMail configures SPF, DKIM, and DMARC for that domain automatically. Your customers will see replies from `[email protected]` with valid authentication records, not a multimail.dev subdomain.
How does gated_send work in a high-volume support environment?
The `list_pending` endpoint supports filtering by metadata fields, so you can query only messages for specific ticket categories, agents, or priority levels. Your approval UI calls `decide_email` with `approve` or `reject` per message. For bulk operations you can loop over `list_pending` results and batch-approve low-risk categories while routing escalations to senior staff.
What happens if a customer replies to a MultiMail-sent message directly?
Replies arrive at the MultiMail mailbox you configured as the sender. MultiMail fires an `email.received` webhook with the full message and the `thread_id` you set on the original send. Your handler uses that `thread_id` to locate the Zendesk ticket and post the reply as a comment, keeping the ticket timeline complete.
Does MultiMail handle unsubscribe requests automatically?
MultiMail adds a List-Unsubscribe header to every outbound message as required by CAN-SPAM and the 2024 Google/Yahoo bulk sender requirements. When a customer clicks unsubscribe or replies with an opt-out, MultiMail fires an `email.unsubscribed` webhook event and blocks further sends to that address. Your handler should also update the Zendesk contact record to reflect the opt-out.
Is there a Zendesk app or marketplace integration?
Not currently. The integration is API-to-API: your application calls both the Zendesk REST API and the MultiMail REST API. This gives you full control over the routing logic, oversight mode selection per ticket type, and the approval UI—none of which fit cleanly into a Zendesk Marketplace app model.

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.