MailKite encrypts your email in transit and at rest, signs every webhook with HMAC-SHA256, and scopes every credential to the smallest surface that still works. Encrypt stored mail to your own PGP or S/MIME key and we hold ciphertext we can't open — or switch on zero-retention passthrough and we store nothing at all. Every claim below maps to something you can verify.
HTTPS-only API, STARTTLS and implicit TLS on submission, IMAPS on 993, STARTTLS at the MX edge, TLS to your webhook.
Turn on at-rest encryption and stored mail is sealed to a public key you hold — PGP/MIME, S/MIME, or our RSA-OAEP envelope. We keep ciphertext we can't open.
Zero-retention passthrough delivers the webhook and persists nothing: no body, no attachments, no delivery record. Your endpoint holds the only copy.
Every delivery carries an HMAC-SHA256 signature over the raw body, with a per-route secret and a ±5-minute replay window.
Account key → one domain → one mailbox app password → a route your agent holds no key for. Each grant revocable on its own.
An inbound agent can reply to its sender and nothing else. Enforced by the toolset it's constructed with, not by asking a model nicely.
Six network surfaces touch your mail. Here is what each one requires, and where a plaintext connection is still possible.
| Surface | Endpoint | Protection |
|---|---|---|
| REST API & MCP | api.mailkite.dev · mcp.mailkite.dev | HTTPS only. Plain-HTTP requests are rejected — the one exception is http://localhost, so you can test a webhook receiver on your own machine. |
| SMTP submission | smtp.mailkite.dev:587 | STARTTLS. AUTH is advertised only once the session is encrypted, so your API key is never offered over a cleartext connection. |
| SMTP submission | smtp.mailkite.dev:465 | Implicit TLS — encrypted from the first byte, for clients that prefer SSL/TLS to STARTTLS. |
| Inbound MX | mx.mailkite.dev:25 | STARTTLS offered to every sending server, on a Let's Encrypt certificate renewed and hot-reloaded automatically. |
| IMAP | imap.mailkite.dev:993 | IMAPS — implicit TLS. There is no cleartext IMAP port to misconfigure. |
| Webhook delivery | your endpoint | Delivered over TLS to your HTTPS URL and signed, on the first attempt and on every retry. |
Where it stops: once a message leaves our submission edge for a receiving server we don't run, its encryption is that receiver's business. That's true of every email provider — TLS between mail servers is opportunistic by design. If you need end-to-end guarantees the transport can't give you, encrypt the payload: that's what the next section is for.
Your webhook is the system of record — MailKite is the pipe. You choose per domain how much of it we hold on to.
The convenience default — enough history to replay and debug a delivery, then it expires on its own.
Sealed to your public key before it's written. Subject, from, to, and timestamps stay plaintext so listings still work.
The webhook fires and nothing is persisted. Attachments arrive inlined as base64 — there's no stored object to link to.
Upload an OpenPGP public key or an S/MIME certificate and we write
standard encrypted MIME — multipart/encrypted
for PGP/MIME, application/pkcs7-mime
for S/MIME. A normal mail client reaching the mailbox over IMAP
decrypts it natively, with the key you already have. There is no
MailKite library in the decryption path, and nothing to migrate off
if you leave.
The original format — a hybrid envelope that wraps a fresh
AES-256-GCM content key with your RSA-OAEP public key — is still
supported and still decryptable with plain WebCrypto. Every official
SDK ships matching encrypt and
decrypt helpers that need no API
key and make no network call.
We validate and store only the public half — an RSA key of at least 2048 bits, or your certificate — plus its fingerprint, so old mail stays readable across a key rotation. We never hold a private key. Lose yours and the mail is gone; that is what makes the guarantee real.
Set up encryption for a domain{
"v": 1,
"keyAlg": "RSA-OAEP-256",
"fp": "a1b2c3…", // which of your keys this was sealed to
"enc": "A256GCM",
"iv": "…", // base64
"wrappedKey": "…", // base64: your public key wrapping the AES key
"ciphertext": "…" // base64: AES-256-GCM of the body
} Inbound attachments are kept for 7 days and then removed by lifecycle rule, on every plan.
Anyone can POST to your webhook URL. Every real delivery carries an
x-mailkite-signature header —
t=<ms>,v1=<hex> — where
v1 is an HMAC-SHA256 over the
timestamp and the raw request body. Verify it and you only ever act
on events that genuinely came from us.
import { MailKite } from "mailkite";
const sig = req.headers["x-mailkite-signature"];
// One call: parses the header, recomputes the
// HMAC over the RAW bytes, compares in constant
// time, and rejects anything outside ±5 minutes.
if (!MailKite.verifyWebhook(sig, req.body, SECRET)) {
return res.sendStatus(401);
}
// ...trusted from here. Sign the raw bytes, not a parsed-and-re-serialized object — key ordering and whitespace change the bytes and break the signature. Every official SDK ships this check, so there is no crypto to hand-roll; the docs also show the plain-standard-library version for six languages if you'd rather not take a dependency.
Most email APIs give you one key that does everything. MailKite gives you a ladder, and every rung is revocable without disturbing the ones above it.
*
Every domain, every mailbox
One key runs all your email: the agent can create domains, set webhooks, send and read everywhere — unlimited domains and mailboxes. For your own trusted automation.
*@agents.yourapp.com
Every mailbox on one domain
The agent sends, reads, and replies on that domain and nothing else — the rest of your account is out of reach. Mint one per agent; revoke one without disturbing the others.
support@yourapp.com
One mailbox
A route hands one address's mail to your agent — that inbox is all it sees and all it replies from. The narrowest grant: the agent never holds a key at all.
A named mk_pw_… credential scoped
to one domain and an address pattern —
support-*, one exact mailbox, or
all of them — over IMAP, the mailbox API, or both. Shown once at
creation and stored only as a SHA-256 hash, so a database read
doesn't hand anyone a working credential. Give one to an agent
instead of your account key.
Dashboard passwords are hashed with PBKDF2-HMAC-SHA-256 at 100,000 iterations, with a random per-password salt, and verified in constant time. The parameters are stored alongside the hash so they can be raised later without a migration. Sign in with Google if you'd rather we never hold a password at all.
Rotation: rotating the account key
invalidates the old one immediately. Domain-scoped keys and app passwords
are revoked individually. Every credential is a bearer token — keep it
server-side, in a secret manager, never in client code or a repo.
Requests must use HTTPS; the only plain-HTTP exception is
http://localhost for testing a
receiver on your own machine.
An agent that reads attacker-controlled email, holds private data, and can send mail is the lethal trifecta in one process. MailKite breaks it by construction and only widens behind proof of identity — enforced by the tools the agent is given, never by instructions in a prompt.
| Level | It can see | It can do | Gate |
|---|---|---|---|
| L0 default | Only the one email it is answering | Reply to that sender, or forward to an owner-controlled address | None needed — safe by construction |
| L1 opt-in | + the rest of this thread, participant-filtered | Same as L0 — reply and forward only | A reply anchor we actually issued, plus a DMARC-verified From that matches it |
| L2 opt-in | + mail where this verified sender participates | Same as L0 — reply and forward only | The same verification gate; the route chooses the read scope |
Account configuration — domains, routes, webhooks, templates, and sending from arbitrary addresses — is never reachable from an inbound agent at any level. That surface belongs to the dashboard assistant, which is authenticated as you.
An inbound agent is constructed with a minimal tool array, not the dashboard's. list_domains, set_webhook, create_route, create_domain and unconstrained send_email don't exist in its world at any level — so no wording of an email can reach them.
from, to and inReplyTo are set by us from the inbound message. The model supplies subject and body only, so it cannot be steered into mailing your data somewhere else or sending as another of your domains.
A forward is checked before it sends against your account email, addresses on domains you've verified, and the route's own forward list. An injected "forward this to attacker@evil.com" is refused by the executor.
A forged In-Reply-To, an anchor we never issued, a From that doesn't match it, a non-passing DMARC, or an expired anchor all drop the run back to L0. Unknown state is L0 too.
Even past the gate, an agent only sees messages the verified sender was actually a party to — so a token that leaked to a CC'd third party still surfaces nothing new.
An anti-loop cap limits how many agent runs a single sender can trigger on your account inside the window, so an auto-responder loop can't run up your bill.
Row-level scoping is the everyday mechanism. The control that actually keeps mail from crossing an account line is that you can't operate on a domain you haven't proven you own.
Domains, routes, messages, deliveries, contacts, templates and keys all hang off your account id. There are no shared rows between tenants.
You can only receive for a domain whose MX you've verified, and only send from one with SPF and DKIM in place. The gate is a single tested function every path runs through.
Inbound attribution honors the recipient domain only when it is verified, and only fires routes owned by that domain's own account. Unverified or unknown recipients are parked as unrouted rather than misrouted.
A domain-scoped key gets a 403 (code key_scope) on creating domains, rotating keys, rotating webhook secrets, or anything billing — not a filtered result, a refusal.
The API runs on Workers, with D1 and R2 behind it. No servers of ours sit in the request path for the API.
The MX, submission and IMAP edges run on hosts we operate, with Let's Encrypt certificates issued over DNS-01 and redeployed into each service on every renewal.
The HTTP path our Worker uses to hand outbound mail to the submission edge is restricted to Cloudflare's published egress ranges, re-applied on every boot, and fails closed when the range list can't be fetched. The requests themselves are HMAC-signed with a 300-second replay window — the firewall is the second lock, not the first.
The MX edge accepts RCPT only for domains the platform actually hosts, checked live. The submission edge only relays after a successful AUTH, which is only offered on an encrypted session.
The MX edge, the submission edge and the IMAP server are open source at github.com/mailkite/server. The parts of our stack that touch your mail on the wire are not a black box you have to take on faith.
A GDPR Article 28 DPA, a named subprocessor list, and a 99.9% uptime SLA on every paid plan — all public, none of it behind a sales call.
Compliant by design
Our subprocessors operate infrastructure that includes the United States. Transfers out of the EEA, UK, or Switzerland are governed by the EU Standard Contractual Clauses and the UK International Data Transfer Addendum, and by the EU-U.S. and UK-U.S. Data Privacy Framework where a subprocessor is self-certified. Message retention follows your plan's window; delete your account and we remove your data within 30 days, except where the law requires us to keep it.
You'd find all of this in a security review anyway. Here it is up front, so you can decide in five minutes instead of three weeks.
We don't hold one today, and we'd rather say so here than have you find out three weeks into a review. If your process requires an audited report, tell us where you are in it and we'll tell you honestly whether we can meet you there.
The v1 at-rest envelope is a hybrid RSA-OAEP + AES-256-GCM construction we built. It uses standard primitives, and the newer PGP/MIME and S/MIME formats use standard libraries — but none of it has had an external crypto review. If you're storing regulated data, get your own before you rely on it.
App passwords are already shown once and stored as a hash. The account API key isn't there yet — hashed, show-once keys are the next step. Until then, treat it like a password: server-side only, in a secret manager, and rotate it whenever you have a reason to.
One account is one tenant. There is no org, no member list, and no read-only role. Domain-scoped keys and app passwords are how you divide access today; finer-grained keys are on the roadmap.
We offer STARTTLS to every sending server, but we don't yet publish an MTA-STS or DANE policy, so a sender that refuses TLS is still accepted rather than dropped. That's how internet mail works by default; the policy layer is on the list.
We don't sign one. If you're handling PHI, zero-retention passthrough keeps it off our storage entirely — but that's a mitigation, not a compliance answer, and you should treat it as such.
Email security [at] mailkite [dot] dev. A real person reads it. Include what you found, how to reproduce it, and what you think the impact is — we'll confirm receipt and tell you what we're doing about it.
By default MailKite stores a parsed copy of each message for your plan's retention window (7 days on Free, up to 365 on Business) on encrypted infrastructure. You can go further per domain: turn on at-rest encryption and the message body is sealed to a public key you hold — an OpenPGP key, an S/MIME certificate, or an RSA public key — before it is written, so MailKite stores ciphertext it has no way to open. Or turn on zero-retention passthrough and MailKite stores nothing at all.
With default retention, yes — the stored copy is readable, which is what makes replay, search, and the dashboard work. With at-rest encryption on for a domain, no: we only ever hold your public key or certificate, so we can encrypt but not decrypt. With zero-retention passthrough, there is nothing stored to read. The live webhook delivery is plaintext over TLS in all three modes, because that is the copy your own code consumes.
Every delivery carries an x-mailkite-signature header of the form t=<ms>,v1=<hex>, where v1 is an HMAC-SHA256 of the timestamp and the raw request body keyed with that route's signing secret. Verify it against the raw bytes — not a re-serialized object — with a constant-time comparison, and reject events whose timestamp is more than about five minutes old. Every official SDK ships a one-call verifyWebhook helper that does all of it, and the same signature is used for live events, retries, and test events.
Rotate it in the dashboard; the old key stops working immediately. To reduce the blast radius before that happens, don't hand your account key to everything: a domain-scoped key can only send, read and manage one domain and is refused on account-level operations, an app password covers a single mailbox pattern over IMAP or the mailbox API and is revocable on its own, and an inbound agent route needs no credential at all.
That is exactly the attack MailKite's inbox agents are built against — an agent that reads attacker-controlled content, holds private data, and can send mail is the classic lethal trifecta. By default a MailKite inbox agent is constructed with two tools: reply, with the recipient pinned server-side to the original sender, and forward, checked against an allowlist you control. It has no access to your other mail, your domains, your routes, or your webhooks. Wider context requires a reply anchor we issued plus a DMARC-verified From that matches it, and anything unverified falls back to the locked-down default.
Every record is keyed to an account, and the load-bearing control is DNS proof of control: you can only receive mail for a domain whose MX you have verified, and only send from a domain with SPF and DKIM in place. Inbound attribution only fires routes owned by the recipient domain's own account, so a route someone else creates can never capture your mail; unverified or unknown recipients are parked as unrouted rather than delivered to the wrong account.
Yes. MailKite offers a GDPR Article 28 Data Processing Addendum covering processor obligations, subprocessors, and international transfers under the EU Standard Contractual Clauses and the UK International Data Transfer Addendum. Our subprocessors are Cloudflare for compute, storage and mail infrastructure, Stripe for billing, and Anthropic for the optional AI features. Card data is entered directly into Stripe's hosted fields and never reaches MailKite's servers.
No. MailKite does not hold a SOC 2 Type II or ISO 27001 report today, and we say so publicly rather than in the twelfth reply of a questionnaire thread. What we offer instead is verifiability: standard cryptography you can check, mail-edge source code you can read at github.com/mailkite/server, per-domain encryption where you hold the only private key, and zero-retention passthrough where there is nothing to audit because there is nothing stored.
Add a domain, paste in a public key, and watch a signed webhook land carrying mail we can't read. Unlimited domains, free, no card.
Ask about MailKite
I can see the page you're on, and I can change it for you.