Get your API key
Privacy & data

Data, retention & encryption

Your webhook is the system of record — MailKite is the pipe. You decide, per domain, how much (if anything) we keep: short auto-expiring retention by default, zero-retention passthrough to keep nothing, or at-rest encryption with your own key so we store it but can't read it.

Retention windows

By default we keep a parsed copy of each message just long enough for you to replay or debug a delivery, then it auto-expires. No action needed.

PlanMessage retention
Free3 days
Pro30 days
Scale90 days
Business365 days

Inbound attachments are stored for 7 days, then deleted by lifecycle rule.

Zero-retention passthrough

Turn this on for a domain and MailKite delivers the webhook but persists nothing: no message body, no attachments, no delivery record. Your webhook holds the only copy. Best for privacy-sensitive inboxes where you never want a server-side copy to exist.

Toggle it on the domain in the dashboard, or via the API:

enable passthrough
curl -X PUT https://api.mailkite.dev/api/domains/DOMAIN_ID/retention \
  -H "authorization: Bearer mk_live_3a9f…" \
  -H "content-type: application/json" \
  -d '{"zeroRetention": true}'

With passthrough on:

  • The first (and only) webhook delivery happens as usual, over TLS, signed.
  • There's no replay and no dashboard history for that domain — nothing was stored to replay.
  • Attachments are inlined into the webhook as base64 content instead of a signed url (there's no stored object to link to):
attachment (passthrough)
{
  "filename": "po.pdf",
  "contentType": "application/pdf",
  "size": 18213,
  "content": "JVBERi0xLjQKJ…"   // base64 of the file — no signed URL, nothing stored
}

At-rest encryption

The middle ground: we keep the message for the normal retention window, but the body is encrypted to your public key before it's written, so MailKite can never read it. Only you hold the private key. Encryption is per-domain and opt-in.

1. Generate a keypair

generate an RSA keypair
# Generate a 2048-bit RSA keypair. Keep private.pem secret — it's the only thing
# that can ever read your encrypted mail. Paste the contents of public.pem into MailKite.
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
openssl rsa -in private.pem -pubout -out public.pem

2. Add the public key to the domain

Paste public.pem into the domain's settings in the dashboard, or PUT it via the API. We validate it (RSA, ≥2048-bit) and store only the public key plus its fingerprint — never a private key.

enable encryption
curl -X PUT https://api.mailkite.dev/api/domains/DOMAIN_ID/encryption \
  -H "authorization: Bearer mk_live_3a9f…" \
  -H "content-type: application/json" \
  -d "$(jq -Rs '{publicKey: .}' public.pem)"
# → { "domain": { …, "enc_key_alg": "RSA-OAEP-256", "enc_fingerprint": "a1b2…" } }

What changes once it's on

  • Your live webhook is unchanged — the first delivery is still plaintext over TLS. If you just consume webhooks, you do nothing.
  • The stored copy of text and html is an encrypted envelope (below). The dashboard shows a locked state; replaying a message delivers the ciphertext for you to decrypt.
  • subject, from, to, and timestamps stay plaintext so listings still work.
  • Attachments are inlined plaintext on the first delivery and not retained (so replays carry no attachments).
Is this hard to use? For the common case, no — if you process webhooks as they arrive, encryption is transparent (you get plaintext over TLS) and the only setup is pasting one public key. You only need the decrypt step below if you read the stored copy: replays, the Messages API, or the dashboard.

The stored envelope

When encryption is on, the text/html you read back from storage is this JSON, not the message:

encrypted envelope
{
  "v": 1,
  "keyAlg": "RSA-OAEP-256",
  "fp": "a1b2c3…",              // which key this was encrypted to
  "enc": "A256GCM",
  "iv": "…",                    // base64
  "wrappedKey": "…",            // base64: your public key wrapping the AES key
  "ciphertext": "…"            // base64: AES-256-GCM of the body
}

Decrypting

Easiest path: every official SDK (0.4.0+) ships encrypt(plaintext, publicKey) and decrypt(envelope, privateKey) helpers — byte-compatible with the at-rest envelope above. Pass the stored text/html envelope and your private key; it returns plaintext. No API key, no network call.

decrypt with the SDK
import { readFileSync } from "node:fs";
import { MailKite } from "mailkite";

const privateKey = readFileSync("private.pem", "utf8");

// The stored text/html is the encrypted envelope; decrypt() returns plaintext.
const text = MailKite.decrypt(message.text, privateKey);
Install Docs →

Not using an SDK? The envelope is a plain hybrid RSA-OAEP + AES-256-GCM format, so you can decrypt it with your private key using standard WebCrypto — no MailKite library required:

decrypt.mjs
import { readFileSync } from "node:fs";
import { webcrypto as crypto } from "node:crypto";

// Load your RSA private key (the public half is what MailKite has).
const pem = readFileSync("private.pem", "utf8")
  .replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
const der = Uint8Array.from(atob(pem), (c) => c.charCodeAt(0));
const privateKey = await crypto.subtle.importKey(
  "pkcs8", der, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"],
);

const b64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));

export async function decrypt(envelopeJson) {
  const e = JSON.parse(envelopeJson);
  const rawKey = await crypto.subtle.decrypt({ name: "RSA-OAEP" }, privateKey, b64(e.wrappedKey));
  const key = await crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["decrypt"]);
  const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: b64(e.iv) }, key, b64(e.ciphertext));
  return new TextDecoder().decode(pt);
}

// const text = await decrypt(message.text);   // the value in 'text'/'html' is the envelope
Encryption is a v1 of a hand-rolled hybrid envelope (RSA-OAEP wrapping AES-256-GCM). If you're storing regulated data, get your own crypto review before relying on it. Lose your private key and the encrypted mail is unrecoverable — that's the point of zero-knowledge.

Uptime SLA

Every paid plan is backed by a 99.9% monthly uptime SLA with service credits. Read the full terms at mailkite.dev/sla.

Which should I use?

ModeWe storeWe can read itReplay
Default retentionParsed message (auto-expires)YesYes
At-rest encryptionEncrypted bodyNoCiphertext
Zero-retention passthroughNothingn/aNo

See also Domains & DNS and Verifying signatures.