MailKite

MailKite for Elixir

Email for every product you ship — receive email as a webhook, send over a verified domain, give an AI agent its own inbox.
The official MailKite library for Elixir.

Docs · Library guide · mailkite.dev · AI agents

Hex

Read-only mirror. This repo is a generated, release-time mirror of the MailKite monorepo (the private source of truth) — development doesn't happen here. Install from Hex and open issues against the MailKite docs.

Install

Add :mailkite to deps in mix.exs:

def deps do
[{:mailkite, "~> 0.13"}]
end

Then mix deps.get. The library has zero third-party runtime dependencies — HTTP is stdlib :httpc/:inets, crypto is stdlib :crypto/:public_key, and JSON uses the built-in JSON module on Elixir 1.18+ (falling back to Jason on older Elixir).

Quickstart

mk = MailKite.new(System.get_env("MAILKITE_API_KEY"))
{:ok, res} =
MailKite.Methods.send(mk, %{
"from" => "hello@myapp.ai",
"to" => "ada@example.com",
"subject" => "Your invoice #1042",
"html" => "<p>Thanks! Receipt attached.</p>"
})

Every method returns {:ok, value} on success (the parsed JSON response) or {:error, %MailKite.Error{}} on any non-2xx response — pattern-match to handle both:

case MailKite.Methods.send(mk, message) do
{:ok, res} -> IO.puts("queued #{res["id"]}")
{:error, %MailKite.Error{status: status, message: msg}} -> IO.puts("failed #{status}: #{msg}")
end

Authentication — API key or OAuth

The credential is always a Bearer token, so an OAuth access token works anywhere an API key does. Server-to-server code → API key; anything that renders on a public URL → OAuth (each user acts as themselves, not through a shared key).

# Server-to-server: a static API key (mk_live_…).
mk = MailKite.new(System.get_env("MAILKITE_API_KEY"))
# OAuth: a static access token…
mk = MailKite.Client.new(access_token: my_oauth_token)
# …or a get_token callback called before each request, so short-lived
# OAuth access tokens stay fresh:
mk = MailKite.Client.new(get_token: fn -> current_session_access_token() end)

Point at a custom base URL with MailKite.new(api_key, "https://api.mailkite.dev") or MailKite.Client.new(access_token: token, base_url: url).

Get an OAuth token from MailKite's authorization server (mcp.mailkite.dev, OAuth 2.1 + PKCE). For browser/native apps use the client-side libraries, which run the whole flow.

Verify inbound webhooks

MailKite.Webhook is entirely local — no network call. Verify the x-mailkite-signature header over the raw request body before trusting an inbound event, then return one of the reply bodies:

if MailKite.Webhook.verify_webhook(signature, raw_body, webhook_secret) do
# process the event…
MailKite.Webhook.reply_ok() # {"status":"ok"}
else
# reject
end
# Control-mode replies a handler can return:
MailKite.Webhook.reply_spam() # {"status":"spam"}
MailKite.Webhook.reply_drop() # {"status":"drop"}
MailKite.Webhook.reply_block_sender() # {"status":"ok","actions":[{"type":"block-sender"}]}

verify_webhook/4 rejects events older than a 5-minute replay window by default; pass a fourth argument (ms, or 0 to disable) to change it.

At-rest encryption

MailKite.Crypto encrypts to an RSA public key and decrypts with the private key. The envelope is byte-compatible with every other MailKite SDK and MailKite's own WebCrypto, so a value encrypted in one language decrypts in another.

envelope = MailKite.Crypto.encrypt("secret note", public_key_pem)
{:ok, plaintext} = MailKite.Crypto.decrypt(envelope, private_key_pem)

Attachments

Upload a file once and reference the returned URL on sends, instead of base64-inlining it every time. Provide the file as a url (re-hosted by MailKite), raw bytes, a local path, or base64 content:

{:ok, up} =
MailKite.Client.upload_attachment(mk, %{
"path" => "/tmp/receipt.pdf",
"filename" => "receipt.pdf"
})
MailKite.Methods.send(mk, %{
"from" => "billing@myapp.ai",
"to" => "ada@example.com",
"subject" => "Your receipt",
"text" => "Attached.",
"attachments" => [%{"filename" => up["filename"], "url" => up["url"]}]
})

API methods

Every endpoint is a function in MailKite.Methods, taking the client first. Names are snake_case (e.g. list_domains/1, get_template/2, send_broadcast/3). The full surface: send, upload_attachment, list_templates, list_base_templates, get_template, create_template, list_domains, create_domain, get_domain, delete_domain, verify_domain, set_webhook, delete_webhook, test_webhook, check_domain_availability, register_domain, list_routes, create_route, delete_route, agent, route, list_messages, get_message, retry_delivery, list_lists, create_list, get_list, update_list, delete_list, list_list_contacts, add_list_contacts, remove_list_contact, list_broadcasts, create_broadcast, get_broadcast, update_broadcast, delete_broadcast, send_broadcast, semantic_search, plus the local MailKite.Webhook and MailKite.Crypto helpers.

List endpoints take a keyword of pagination options:

{:ok, msgs} = MailKite.Methods.list_messages(mk, before: 1_750_000_000_000, limit: 50, search: "invoice")

Use it from an AI agent — MCP + Agent connectors

MailKite speaks the Model Context Protocol: every API method is a tool your AI assistant (Claude, Cursor, …) can call. Full guide: https://mailkite.dev/docs/ai-agents.

Hosted (recommended) — one-click OAuth, no key to copy:

claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp

Local (static key, offline / CI):

{ "mcpServers": { "mailkite": { "command": "npx", "args": ["-y", "@mailkite/mcp"], "env": { "MAILKITE_API_KEY": "mk_live_…" } } } }

All MailKite libraries

Same contract, every language — pick the one for your stack (full list: https://mailkite.dev/docs/libraries):

LibraryRepoDistribution
MailKite for Node.jsmailkite-nodenpm
MailKite for Pythonmailkite-pythonPyPI
MailKite for Rubymailkite-rubyRubyGems
MailKite for Elixir (this repo)mailkite-elixirHex
MailKite for Javamailkite-javaMaven Central
MailKite for PHPmailkite-phpPackagist
MailKite for Gomailkite-goGo modules
@mailkite/climailkite-clinpm
@mailkite/mcpmailkite-mcpnpm

Generated from the shared MailKite API contract. © MailKite. MIT licensed.