Security

Security you can check yourself

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.

At a glance

Six controls, and where each one actually lives

TLS on every hop

HTTPS-only API, STARTTLS and implicit TLS on submission, IMAPS on 993, STARTTLS at the MX edge, TLS to your webhook.

Encrypted to your key

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.

Or store nothing

Zero-retention passthrough delivers the webhook and persists nothing: no body, no attachments, no delivery record. Your endpoint holds the only copy.

Signed webhooks

Every delivery carries an HMAC-SHA256 signature over the raw body, with a per-route secret and a ±5-minute replay window.

Credentials that narrow

Account key → one domain → one mailbox app password → a route your agent holds no key for. Each grant revocable on its own.

Agents locked down by default

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.

In transit

Encrypted on every hop we control

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.

At rest

Keep less, or keep it sealed to your key

Your webhook is the system of record — MailKite is the pipe. You choose per domain how much of it we hold on to.

Default retention

We store
Parsed message, auto-expiring
We can read it
Yes
Replay
Yes

The convenience default — enough history to replay and debug a delivery, then it expires on its own.

At-rest encryption

We store
Encrypted body
We can read it
No
Replay
Ciphertext

Sealed to your public key before it's written. Subject, from, to, and timestamps stay plaintext so listings still work.

Zero-retention passthrough

We store
Nothing
We can read it
n/a
Replay
No

The webhook fires and nothing is persisted. Attachments arrive inlined as base64 — there's no stored object to link to.

Standard formats, standard tools

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
the stored envelope
{
"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
}

Default retention windows

Free
7 days
Pro
30 days
Scale
90 days
Business
365 days

Inbound attachments are kept for 7 days and then removed by lifecycle rule, on every plan.

Signed webhooks

Your endpoint is public. The signature isn't.

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.

  • One scheme, one secret: inbound email.received and outbound email.* tracking events are signed identically.
  • Each webhook route carries its own signing secret, so revoking one doesn't touch the others.
  • A ±5-minute freshness window plus a constant-time compare blocks replays and timing probes.
  • Live deliveries, automatic retries and the dashboard test event all carry the same signature — you can prove your verification works before going live.
Verify signatures in your language
verify.js
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.

Credentials

Hand out the narrowest thing that works

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.

Account key

*

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.

Domain-scoped key

*@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.

Agent route

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.

App passwords

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.

Account passwords

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.

Agent access control

An inbox agent starts with almost no reach

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.

The toolset is the boundary

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.

The reply address is pinned server-side

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.

Forwarding runs against an allowlist

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.

Widening needs proof, and fails closed

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.

Reads are participant-filtered

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.

One sender can't drive it in circles

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.

Tenant isolation

DNS proof of control is the hard boundary

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.

Every record is keyed to your account

Domains, routes, messages, deliveries, contacts, templates and keys all hang off your account id. There are no shared rows between tenants.

Proof of control gates mail, not just the UI

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.

A foreign route can never catch your mail

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.

Scoped keys are fenced at the account line

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.

Infrastructure

The parts that touch your mail are readable

Cloudflare for compute and storage

The API runs on Workers, with D1 and R2 behind it. No servers of ours sit in the request path for the API.

Mail edges on dedicated hosts

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 internal inject port is firewalled and signed

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.

No open relay, by two independent checks

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.

You can read the code

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.

Compliance & data handling

The paperwork, already written

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

GDPR Compliant
PCI Compliant
CCPA Compliant

Subprocessors

  • Cloudflare — compute, storage (D1, R2), and email send/receive infrastructure.
  • Stripe — payments and subscription billing. Stripe is a PCI DSS Level 1 service provider; card numbers go into Stripe's hosted fields and never touch MailKite's servers.
  • Anthropic — powers the optional inbox-agent and assistant features, invoked only when you use AI.

Transfers, retention & deletion

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.

What we don't have yet

The part most security pages leave out

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.

No SOC 2 or ISO 27001 report

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 RSA envelope hasn't been independently audited

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.

Show-once account keys are still on the roadmap

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.

No teams, roles, or per-user permissions

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.

Inbound TLS is opportunistic

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.

No HIPAA BAA

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.

Disclosure

Found something?

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.

While you're testing

  • Use your own account and your own domains. Don't access, modify, or store anyone else's data — if a bug gives you someone's mail, stop and tell us rather than confirming how much you can reach.
  • No automated scanning or load generation that degrades the service for other people. We'd rather you told us what you want to try.
  • Give us a reasonable window to ship a fix before publishing. We don't run a paid bounty today, and we'll say so plainly rather than string you along — credit in the changelog if you want it.

Questions

Is email encrypted at rest in MailKite?

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.

Can MailKite read my email?

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.

How do I verify a MailKite webhook is genuine?

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.

What happens if my API key leaks?

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.

Can an AI agent with an inbox be tricked into leaking my data?

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.

How does MailKite keep one customer's mail from reaching another?

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.

Is MailKite GDPR compliant, and do you sign a DPA?

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.

Does MailKite have a SOC 2 report?

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.

Try it against your own domain

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.