Skip to content
MeridFlow AiFlow v8.x • self-hosted

Mailbox connections

An agent can watch a real inbox for new mail and react on its own judgment: reply, send a WhatsApp or Telegram message, place a call, or take no action and just log why. It's the same menu of reactions a trigger has, but decided by the agent's own reasoning rather than a fixed rule. Each agent can have at most one mailbox, the email equivalent of its phone number.

Two ways to connect a mailbox:

  • A real IMAP and SMTP account. Works with any mail provider. AiFlow polls it on an interval, and a reply goes out over SMTP using that mailbox's own credentials.
  • Resend's own inbound receiving (recommended). Resend delivers new mail as a webhook instead of AiFlow polling for it, so there's no password to store, and a reply goes out through the same Resend account already configured for outbound mail on this deployment. This requires RESEND_API_KEY and RESEND_WEBHOOK_SECRET set on the deployment. See Credentials for the full setup walkthrough: the receiving subdomain, the MX record, and registering the webhook.

A newly connected mailbox never reacts to whatever's already sitting in the inbox, only to mail that arrives from the moment it's connected onward. Connecting an existing support inbox with years of history in it won't trigger a reply to every old message.

Memory across a conversation

Turn on cross_session_lookback_enabled (see Agents) and the agent recalls its last conversation with a sender the moment a new email from that same address arrives, using the same recap mechanism phone and widget sessions already rely on. A follow-up email ("thanks, one more question...") gets a short summary of what was already discussed folded into the agent's context before it starts, instead of a blank slate; a sender it's never seen before gets none. Each inbound email is still its own independent agent run: this carries a recap forward, it isn't live multi-turn memory within one exchange.

A reply sent with reply_to_email also carries proper In-Reply-To and References headers back to the message it's answering, so it threads under the original in the recipient's own mail client (Gmail, Outlook, and so on) instead of showing up as a disconnected new email.

Admin role required

GET needs any logged-in admin. PUT and DELETE need Owner or Admin.

Field reference

Field Type Required Default Description
provider string No "imap_smtp" "imap_smtp" or "resend_inbound".
enabled boolean No true Whether the agent is currently watching this mailbox.
from_display_name string or null No null The display name a reply is sent as, e.g. "Acme Support".
imap_host/imap_username string Yes, for imap_smtp None The mailbox's IMAP server and login.
imap_password string Yes, for imap_smtp, on create None Write-only. Omit on an update to keep the currently stored password.
imap_port/imap_use_ssl integer/boolean No 993/true
folder string No "INBOX" The IMAP folder to watch.
poll_interval_seconds integer No 60 How often this mailbox is checked. imap_smtp only, ignored for resend_inbound.
smtp_host/smtp_username string Yes, for imap_smtp None Where a reply is sent from.
smtp_password string Yes, for imap_smtp, on create None Write-only, same omit-to-keep behavior as imap_password.
smtp_port/smtp_use_tls integer/boolean No 587/true
resend_receiving_address string Yes, for resend_inbound None The address mail actually arrives at, e.g. support@inbound.your-domain.com.

Response shape

GET and PUT both return this shape. Password fields are write-only and never echoed back.

{
  "id": 1,
  "agent_id": 1,
  "provider": "resend_inbound",
  "enabled": true,
  "from_display_name": "Acme Support",
  "imap_host": null,
  "imap_port": 993,
  "imap_use_ssl": true,
  "imap_username": null,
  "folder": "INBOX",
  "poll_interval_seconds": 60,
  "smtp_host": null,
  "smtp_port": 587,
  "smtp_use_tls": true,
  "smtp_username": null,
  "resend_receiving_address": "support@inbound.your-domain.com",
  "last_seen_uid": null,
  "last_polled_at": null,
  "created_at": "2026-01-15T10:31:00Z"
}

last_seen_uid and last_polled_at are imap_smtp-only bookkeeping, always null for a resend_inbound connection since that provider has no polling loop to track.

Configure a mailbox

PUT /api/v1/agents/{agent_id}/mailbox-connection

Creates the connection if none exists yet, or updates the same one in place otherwise. Since there's at most one per agent, this single endpoint covers both cases.

curl -X PUT https://api.your-domain.com/api/v1/agents/1/mailbox-connection \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -d '{
    "provider": "resend_inbound",
    "resend_receiving_address": "support@inbound.your-domain.com",
    "from_display_name": "Acme Support"
  }'
import httpx

response = httpx.put(
    "https://api.your-domain.com/api/v1/agents/1/mailbox-connection",
    headers={"Authorization": f"Bearer {admin_token}"},
    json={
        "provider": "resend_inbound",
        "resend_receiving_address": "support@inbound.your-domain.com",
        "from_display_name": "Acme Support",
    },
)
response.raise_for_status()
connection = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/mailbox-connection",
  {
    method: "PUT",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${adminToken}`,
    },
    body: JSON.stringify({
      provider: "resend_inbound",
      resend_receiving_address: "support@inbound.your-domain.com",
      from_display_name: "Acme Support",
    }),
  },
);
const connection = await response.json();

An IMAP and SMTP connection looks the same, with the provider-specific fields swapped in:

{
  "provider": "imap_smtp",
  "imap_host": "imap.example.com",
  "imap_username": "support@example.com",
  "imap_password": "the-real-password",
  "smtp_host": "smtp.example.com",
  "smtp_username": "support@example.com",
  "smtp_password": "the-real-password",
  "from_display_name": "Acme Support"
}

Get the current connection

GET /api/v1/agents/{agent_id}/mailbox-connection
curl https://api.your-domain.com/api/v1/agents/1/mailbox-connection \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.get(
    "https://api.your-domain.com/api/v1/agents/1/mailbox-connection",
    headers={"Authorization": f"Bearer {admin_token}"},
)
response.raise_for_status()
connection = response.json()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/mailbox-connection",
  { headers: { Authorization: `Bearer ${adminToken}` } },
);
const connection = await response.json();

Returns 404 if this agent has no mailbox connected yet.

Delete a connection

DELETE /api/v1/agents/{agent_id}/mailbox-connection
curl -X DELETE https://api.your-domain.com/api/v1/agents/1/mailbox-connection \
  -H "Authorization: Bearer $ADMIN_TOKEN"
import httpx

response = httpx.delete(
    "https://api.your-domain.com/api/v1/agents/1/mailbox-connection",
    headers={"Authorization": f"Bearer {admin_token}"},
)
response.raise_for_status()
const response = await fetch(
  "https://api.your-domain.com/api/v1/agents/1/mailbox-connection",
  {
    method: "DELETE",
    headers: { Authorization: `Bearer ${adminToken}` },
  },
);

Returns 204 No Content, even if none existed, since deleting is idempotent. Set "enabled": false via PUT instead if you want to temporarily stop the agent watching its inbox without losing the configuration.

The reply_to_email tool

Enable it on the agent alongside a connected mailbox, and it can reply to an inbound message through that mailbox's own address. It's distinct from the platform-wide send_email tool. See Tools for how any built-in tool is turned on for an agent.

Next

  • Tools: the full built-in catalog, including reply_to_email.
  • Triggers: the same reply, WhatsApp, Telegram, call, or silent-action menu, decided by a fixed rule instead of the agent's own judgment.
  • Agents & channels: how a mailbox fits alongside the widget, phone, and events as a way to reach an agent.