Email channel
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
Email channel
A mailbox you already own — SMTP or a hosted provider for outbound, provider inbound-parse webhooks for replies. Chatly never runs an SMTP server.
Chatly does not run an SMTP server. Email is the channel where every shortcut bites you eventually — deliverability rules, bounces, DKIM, complaints — so instead of building yet another half-baked mail stack, we speak to the mailbox or the transactional provider you already trust.
Info — Why BYOK email
Your provider already owns your sending reputation, your DKIM keys, and your inbound parsing. We piggy-back on that. You keep your sender domains; we never re-deliver mail from our infrastructure.
The two halves are configured separately
This is the single most important thing to understand before you start, and the setup form does not spell it out:
Outbound is resolved from the channel config — an SMTP host, or an API key for one of the hosted providers listed below.
Inbound arrives only by a hosted provider POSTing a parsed message to one of our four receiver routes. Everything is matched back to your channel by the inbound address.
Warning — The mailbox provider selector does not fetch mail
The setup form asks for a Mailbox provider (
gmail/outlook/imap) and, for generic IMAP, animapHostandimapPort. Those two values are stored and nothing reads them — there is no IMAP poller in the product. Picking a mailbox provider configures your outbound credentials and nothing else. If you want replies to reach your inbox you must wire one of the inbound receivers below.
Create the channel
Channels → New → Email. The form collects:
Channel config (Email) — what the setup form writes
Name | Type | Description |
|---|---|---|
|
| The address customers write to. This is the routing key for every inbound email — the receiver matches the envelope recipient against |
|
|
|
|
| Collected for |
|
| Collected for |
|
| Collected for |
|
| Falls back to 587 when unset or unparseable. Implicit TLS is inferred when the port is 465. Default: |
|
| Mailbox login. Used as the SMTP auth user, and as the last-resort From address. |
|
| Mailbox password or app password. Encrypted at rest. |
|
| Under Inbound relay credentials. The HTTP Basic pair the Postmark inbound webhook presents. The password half is encrypted at rest; the username half stays readable so you can check it against Postmark's console. |
|
| Under Inbound relay credentials. The HTTP Basic pair embedded in the SendGrid Inbound Parse destination URL. |
|
| Under Inbound relay credentials. Verifies the HMAC Mailgun signs each forwarded message with. Doubles as the outbound Mailgun credential when paired with |
|
| Under Inbound relay credentials. Optional second factor for SES — see the SES note below. Leave blank unless you also add the header to the SNS subscription. |
Info — Relay credentials are a separate axis from the mailbox provider
Mailbox provider picks how we send. Inbound relay credentials configure how a hosted provider hands mail to us. A workspace can use both, and the fields are shown together rather than gated on the provider selector for exactly that reason.
POST /v1/channels — Bearer token
POST /v1/channels
Authorization: Bearer …
Idempotency-Key: 4f1c…
Content-Type: application/json
{
"type": "email",
"name": "Support inbox",
"publicId": "acme-support-email",
"config": {
"address": "[email protected]",
"provider": "imap",
"smtpHost": "smtp.acme.com",
"smtpPort": 587,
"username": "[email protected]",
"password": "…"
}
}Warning — publicId is required and you choose it
POST /v1/channelsrejects a body withoutpublicId. It must be unique across the deployment, is 1–64 characters, and cannot be changed later — external systems key off it.
Outbound
The sender resolves a route in this order:
SMTP — if
smtpHostis set, mail goes out over SMTP usingsmtpUser/smtpPasswhen present, otherwiseusername/password.A hosted HTTP provider — chosen by
emailProvider, or inferred from whichever credential key is present.Postmark — the historical default, used when
postmarkApiTokenis set and nothing above matched.
Hosted-provider OUTBOUND credential keys (set via the API; the setup form offers only mailgunApiKey, which is shared with inbound)
Name | Type | Description |
|---|---|---|
|
| Names the provider explicitly. Otherwise the first credential key found below wins. |
|
| Postmark server API token. |
|
| SendGrid API key. |
|
| Both required — Mailgun's send endpoint is per-domain. |
|
| All three required. Signed with SigV4. |
|
| Mailjet authenticates with a key/secret pair. |
|
| SocketLabs addresses the server by id. |
|
| Scaleway TEM is project-scoped. |
|
|
|
|
| Gmail needs |
The From address
There is no sendingAddress field. The From address is resolved as:
config.from, if set.config.address— the inbound address, so replies thread back onto the same channel.config.username, if it parses as an email address.
If none of the three is usable the send fails rather than emitting an unroutable sender. A half-configured Mailgun or Mailjet credential fails with <provider>_incomplete_config before the provider is called.
Inbound
Point your provider's inbound parse webhook at the matching receiver. Each verifies the provider's own credential, then resolves the channel by the recipient address.
Inbound receiver endpoints
Name | Type | Description |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
POST /v1/email/inbound/postmark — Public
POST /v1/email/inbound/sendgrid — Public
POST /v1/email/inbound/mailgun — Public
POST /v1/email/inbound/ses — Public
Warning — No credentials on the channel means no inbound
A receiver whose credential keys are absent answers
401and refuses the mail rather than accepting it unverified. Set the Postmark, SendGrid and Mailgun keys above under Inbound relay credentials on the email channel's setup form (or withPATCH /v1/channels/{id}) before you repoint MX — otherwise the first messages your provider forwards are rejected and, depending on the provider, retried for a while and then dropped.
Wiring inbound
Postmark
Postmark → Servers → Inbound stream → Settings:
Webhook URL →
https://api.chatlychat.com/v1/email/inbound/postmarkWebhook authentication → Basic Auth → the user/pass you stored as
postmarkUser/postmarkPassunder Inbound relay credentials on the channel's setup form.Inbound domain → the domain whose mail you want filed here.
Update your MX to point at inbound.postmarkapp.com.
SendGrid
SendGrid → Settings → Inbound Parse → Add host & URL:
Host →
inbound.acme.comDestination URL →
https://api.chatlychat.com/v1/email/inbound/sendgridBasic Auth → embed the
sendgridUser/sendgridPassyou stored under Inbound relay credentials on the channel's setup form in the URL.
Update MX to mx.sendgrid.net.
Mailgun
Mailgun → Sending → Domains → Receiving → Routes → New route:
Match recipient:
match_recipient(".*@inbound.acme.com")Forward:
forward("https://api.chatlychat.com/v1/email/inbound/mailgun")Stop: enabled (so the message isn't double-handled)
We verify Mailgun's HMAC against config.mailgunApiKey — the Mailgun API key field under Inbound relay credentials on the channel's setup form.
AWS SES
SES → Email receiving → Rule sets → Create rule.
Add an SNS topic action; create the topic if needed.
SNS → Topics → Subscribe:
Protocol: HTTPS
Endpoint:
https://api.chatlychat.com/v1/email/inbound/sesSNS POSTs a
SubscriptionConfirmation; we verify its signature, confirm thatSubscribeURLreally is an Amazon host, and auto-confirm.
Set MX records to point at SES's inbound mail server.
Info — The recipient address must match the channel exactly
Resolution is
lower(config->>'address') = lower(recipient). No wildcards, no plus-address stripping, no domain matching. Mail for an address no channel claims is answered404 no channel for that address.
DNS for sending + inbound
A typical setup:
;; Sending (SPF + DKIM + DMARC)
acme.com. TXT "v=spf1 include:spf.mtasv.net ~all"
pm._domainkey.acme.com. TXT "k=rsa; p=MIGfMA0GCSqGSIb3..."
_dmarc.acme.com. TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
;; Inbound (MX → provider)
inbound.acme.com. MX 10 inbound.postmarkapp.com.Use Domain Connect for one-click DNS at supported registrars. Otherwise add records manually.
Tip — DMARC is not optional
Gmail and Yahoo require DMARC alignment for any sender doing more than 5,000 messages/day to their users. Start with
p=none, inspect reports, then move top=quarantineorp=reject.
Threading
Inbound mail threads on the RFC 5322 header chain only:
The root of the
Referenceschain — the first non-empty entry. It is stable for the life of a thread, so a reply lands on the right conversation even after that conversation was closed.Otherwise
In-Reply-To.Otherwise the message's own
Message-ID, which starts a new thread.
That value becomes the conversation's external_conversation_id. There is no plus-addressing scheme and no subject-similarity fallback: a client that strips References and In-Reply-To starts a new conversation.
Replies to a ticket side conversation are the one exception — our outbound side-conversation mail carries an HMAC-signed Message-ID, and a reply echoing it is filed on the side conversation instead of opening a thread for the vendor.
Attachments
Inbound attachments are carried through by URL, as the provider hosted them. Entries without a url are dropped, because the agent UI would render them as a broken link.
Warning — No download, no scanning, no size cap
There is no MIME-sniffing, no executable-extension blocklist, no virus scanning and no re-hosting step on the inbound email path. Files are surfaced to agents as links to the provider's storage. Treat inbound attachments the way you would treat any attachment on an unfiltered mailbox.
Delivery events — bounces, complaints, opens, clicks
Provider event webhooks POST to a different route from inbound parse:
POST /v1/webhooks/email/{provider} — Public
{provider} is one of postmark, sendgrid, mailgun, ses, brevo. Anything else answers 404. Events normalise to delivered, opened, clicked, bounced, spam, unsubscribed, dropped, deferred, deduplicated on <provider>:<providerMessageId>:<eventType>:<unix>, and are matched back to a message through messages.metadata.externalMessageId.
Warning — These verify against deployment-wide env vars, not per-channel keys
Unlike inbound parse, the tracking receiver checks a platform secret:
POSTMARK_WEBHOOK_BASIC,SENDGRID_WEBHOOK_BASIC,MAILGUN_API_KEY,SES_TRACKING_SHARED_SECRETorBREVO_WEBHOOK_SECRET. If the relevant one is unset, the endpoint rejects every event in production. Self-hosters must set these before pointing a provider at the route.
Per-message events are readable back through GET /v1/messages/{id}/email-events.
Consent and suppression
Email is opt-out: an address with no recorded opt-out may be mailed. contacts.unsubscribedAt is the global "do not contact" flag, and per-channel consent lives on contact_channel_consent with a state of granted or revoked. Campaign sending is the surface that reads it; agent replies inside an open conversation are not gated on it.
Self-hosted notes
Even on a fully self-hosted install, you still bring your own mail path. We do not run Haraka, Postfix, or any SMTP server. smtp-sender is an SMTP client — it connects out to your relay.
If your requirement is "all email egress goes through our own infrastructure," point smtpHost at your own relay and leave every hosted-provider key unset.
Troubleshooting
Warning — Outbound succeeds but replies never appear
In order of likelihood: no inbound receiver is wired at all (the mailbox provider selector does not fetch mail); the channel's
addressdoes not exactly match the envelope recipient; or the provider credential keys are missing, so the receiver is answering401.
Warning — Every outbound email fails on a fully filled-in channel
Check that
smtpHostis set — without it the sender falls through to the HTTP-provider path and, with no provider key configured, gives up. Also check the From resolution: nofrom, noaddressand a non-emailusernameis a hard failure by design.
Warning — Threading breaks after one reply
An email client or auto-responder stripped
ReferencesandIn-Reply-To. There is no fallback signal, so the reply opens a new conversation.