answerLoopsanswerLoops Docs
Self-Hosting

Email Channel Setup

Route inbound support emails through the answerLoops AI pipeline.

How it works

Email doesn't poll an inbox. In the hosted flow, answerLoops verifies a customer-owned domain with Resend receiving; Resend posts signed email.received events to the webhook, answerLoops retrieves the full message, and the AI pipeline triages, embeds, and answers it. Replies go directly to the sender via Resend with proper RFC 5322 threading (Message-ID / In-Reply-To / References).

For self-hosted deployments, you can use the same Resend receiving flow or keep an existing provider that can POST inbound messages to the webhook with the per-organization legacy secret.

Setup

1. Prerequisites

  • RESEND_API_KEY and RESEND_FROM must be set (used for outbound replies)
  • A Resend domain configured with sending and receiving capabilities, or a supported inbound provider for the legacy path

2. Configure in answerLoops Settings

Under Integrations → Email, enter:

  • Allowed sender addresses/domains (optional) — comma-separated list of emails or domains to accept (e.g. example.com, [email protected]). Leave blank to accept all inbound email.
  • Escalation email (optional) — referenced in replies when AI confidence is below threshold.
  • Confidence threshold — 0–1, default 0.8.

Choose Use your own domain and add the DKIM, return-path/SPF, and inbound MX records shown in Settings. Once the domain is verified, customers send mail to support@yourdomain.

3. Configure your receiving webhook

Point your provider's inbound webhook at:

POST https://yourapp.com/api/email/ingest

Resend sends signed email.received events to that endpoint. Set RESEND_WEBHOOK_SECRET to the signing secret from Resend. The route uses the recipient domain to find the organization, then retrieves the full message with RESEND_API_KEY.

For a legacy provider, set the custom secret header:

X-Email-Webhook-Secret: <your webhook secret>

Provider-specific guides

SendGrid Inbound Parse

  1. Settings → Inbound Parse → Add Host & URL
  2. Enter your domain and https://yourapp.com/api/email/ingest
  3. Set the X-Email-Webhook-Secret header via your SendGrid HTTP POST settings

Mailgun Routes

  1. Sending → Routes → Create Route
  2. Match filter: match_recipient("[email protected]")
  3. Action: forward("https://yourapp.com/api/email/ingest")
  4. Add X-Email-Webhook-Secret in the route headers

Postmark Inbound

  1. Message Streams → Inbound → Settings
  2. Inbound webhook URL: https://yourapp.com/api/email/ingest
  3. Set the X-Email-Webhook-Secret header in Postmark webhook settings

Cloudflare Email Routing

  1. Email → Email Routing → Rules
  2. Forward to a Worker that POSTs to your webhook URL with the secret header

Verify

Send an email to your address. A ticket should appear in /tickets within a few seconds with source badge "Email from [email protected]". Reply to the AI's answer from your own inbox — it should append to the same ticket rather than opening a new one.

Reliability

A few things worth knowing about how the pipeline handles the messy realities of email:

  • Idempotent by construction. Every inbound email is keyed on its RFC Message-ID, so a provider webhook retry is a no-op, never a duplicate ticket.
  • Mail-loop guarded. Auto-Submitted, Precedence, X-Auto-Response-Suppress, List-Id, and no-reply sender patterns are all detected and rejected before a reply is ever generated. A per-sender reply throttle backstops anything that slips through.
  • Spam-tolerant, fail-open. Provider spam signals are honored when present, but their absence never blocks a legitimate email.
  • HTML fallback. HTML-only emails are converted to plain text rather than silently dropped.
  • Delivery-status visibility. Bounces, spam complaints, and delivery failures on outbound replies are tracked against the ticket and surfaced as a badge on the ticket detail page.
  • Rate-limited per org. A compromised or misbehaving upstream account can't burn one org's AI spend or flood the ticket queue for everyone else.

Custom domain (verified sending)

An org can verify a domain it owns so outbound replies send with that domain in From: instead of the platform default — see Custom domain (verified sending) for the customer-facing flow. Under the hood this uses Resend's Domains API (domains.create/get/remove), backed by an email_domains table (one verified domain per org for v1) and an integrations.email_send_method discriminator ('platform' | 'oauth' | 'domain') that lib/email/reply.ts branches on when choosing the From: address. RESEND_API_KEY needs Domains API access in addition to Sending — no separate env var is required for this feature beyond that existing key.

Connect Gmail (send-only OAuth)

An org can connect its own Gmail mailbox so outbound replies send through it directly instead of the platform default or a verified domain — see Connect Gmail for the customer-facing flow. Requires a Google Cloud OAuth client requesting only the gmail.send scope (a sensitive, not restricted, scope — it triggers Google's standard OAuth consent-screen review, not the multi-week CASA security assessment; still submit for review as soon as the client/consent screen exists if you plan to let real external users connect). An email_oauth_connections table stores the encrypted access/refresh token pair (one connection per org for v1); integrations.email_send_method's 'oauth' value routes lib/email/reply.ts through lib/email/gmail.ts instead of Resend. Token refresh is handled automatically on send; a dead refresh token (password change, admin revocation, ~6 months inactivity) is detected reactively on the next send attempt, flips the connection to disconnected, and emails the org's admins the same day with a reconnect link — replies fall back to the platform default in the meantime rather than failing silently.

Manual setup required (this is not automatic): create a Google Cloud project, configure an OAuth consent screen, create OAuth 2.0 credentials requesting the gmail.send scope, and add the app's /api/email/gmail/callback URL as an authorized redirect URI. Submit the consent screen for Google's review before external users (not just your own testing) can connect.

Connect Outlook (send-only OAuth)

The Microsoft equivalent of the Gmail flow above — same email_oauth_connections table, same integrations.email_send_method discriminator, routed through lib/email/outlook.ts instead. Requires a Microsoft Entra app registration requesting only delegated Mail.Send (offline_access alongside it, to get a refresh token). At most one OAuth mailbox connection can exist per org — connecting Outlook while Gmail is connected (or vice versa) replaces it, since email_oauth_connections.orgId is unique.

Manual setup required: register an app in Microsoft Entra ID, add a client secret, configure the delegated Mail.Send and offline_access API permissions, and add the app's /api/email/outlook/callback URL as a redirect URI under the app's Authentication settings. Microsoft additionally requires Partner Center publisher verification before real external users (not just your own tenant) can consent without hitting a step-up-consent block — this is a separate, slower, account-level process from just registering the app. Start it early if you plan to offer this to real customers; internal testing against your own tenant works without it.

Unlike Gmail's send path (which mints its own RFC Message-ID and sets In-Reply-To/References directly), Microsoft Graph's sending API doesn't support setting standard headers on outbound mail the same way — lib/email/outlook.ts creates a draft, sends it, then reads back the real Message-ID Graph assigned for answerLoops's own threading records. In-Reply-To is set best-effort via a MAPI extended property; References has no equivalent and is not sent. This is a deliberate, documented per-provider difference, not a bug.

Environment variables

VariableWhat it is
RESEND_API_KEYRequired for outbound replies and for the custom-domain verification flow (Domains API access)
RESEND_FROMDefault reply-from address (e.g. [email protected])
RESEND_WEBHOOK_SECRETSvix signing secret for Resend's outbound delivery-status webhooks (whsec_..., email.bounced/email.complained/etc.) — bounce/complaint tracking is unavailable without it, but sending and inbound ingest both still work
GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRETGoogle Cloud OAuth client credentials for the Connect Gmail flow. Without these, that feature is unavailable but nothing else breaks
GMAIL_REDIRECT_URIOptional override for the OAuth callback URL; defaults to <AUTH_URL or NEXTAUTH_URL>/api/email/gmail/callback
OUTLOOK_CLIENT_ID / OUTLOOK_CLIENT_SECRETMicrosoft Entra app registration credentials for the Connect Outlook flow. Without these, that feature is unavailable but nothing else breaks
OUTLOOK_REDIRECT_URIOptional override for the OAuth callback URL; defaults to <AUTH_URL or NEXTAUTH_URL>/api/email/outlook/callback

The legacy inbound webhook secret is generated automatically and stored per organization. Hosted customers do not configure it; Resend receiving uses the platform webhook signing secret instead.

Notes

  • Quoted reply chains are stripped — only the new message content is ingested.
  • Subject is prepended to the body so triage has full context.
  • Messages under 10 characters are ignored.
  • Replies set Message-ID, In-Reply-To, and References for correct email-client threading.

On this page