Email infrastructure you can stop thinking about
We run the mail infrastructure your product depends on, so you can go build it.
Why teams choose mailkube
Made for developers
POST to the REST API, or relay through SMTP from whatever you already run. Templates, tags and topics travel over SMTP headers. An existing system gets them without a rewrite.
Deliverability expertise
The routing is run by people who have spent more than fifteen years on email infrastructure and inbox placement.
Less to build yourself
Templates, scheduled sending, webhooks, topics and tags ship with the product.
GDPR compliant, hosted in Europe
Customer data is stored at rest in the European Union, and operational access to it happens from inside the EU.
Integrate in minutes
Send your first email in minutes. Official SDKs for six languages, a CLI for your terminal, and SMTP for anything that already sends mail.
Any language works. Six ship an official SDK, and everything else sends over HTTP or SMTP.
import { Mailkube } from "@mailkube/mailkube-node";
const client = new Mailkube({ apiKey: "mk_<key_id>_<secret>" });
const email = await client.emails.send({
from: "Acme <hello@yourdomain.com>",
to: "customer@example.com",
subject: "Hello world",
html: "<p>It works!</p>",
});
console.log(email.id);
import { Mailkube } from "@mailkube/mailkube-node";
// The key arrives per request through the env binding,
// so build the client inside the handler.
export default {
async fetch(request: Request, env: { MAILKUBE_API_KEY: string }) {
const client = new Mailkube({ apiKey: env.MAILKUBE_API_KEY });
await client.emails.send({
from: "Acme <hello@yourdomain.com>",
to: "customer@example.com",
subject: "Hello world",
html: "<p>It works!</p>",
});
return new Response(null, { status: 204 });
},
};
import { Mailkube } from "npm:@mailkube/mailkube-node";
const client = new Mailkube(); // reads MAILKUBE_API_KEY
await client.emails.send({
from: "Acme <hello@yourdomain.com>",
to: "customer@example.com",
subject: "Hello world",
html: "<p>It works!</p>",
});
// bun add @mailkube/mailkube-node
import { Mailkube } from "@mailkube/mailkube-node";
const client = new Mailkube(); // reads MAILKUBE_API_KEY
await client.emails.send({
from: "Acme <hello@yourdomain.com>",
to: "customer@example.com",
subject: "Hello world",
html: "<p>It works!</p>",
});
from mailkube import Mailkube
client = Mailkube(api_key="mk_<key_id>_<secret>")
email = client.emails.send(
from_="Acme <hello@yourdomain.com>",
to="customer@example.com",
subject="Hello world",
html="<p>It works!</p>",
)
print(email.id)
from contextlib import asynccontextmanager
from fastapi import FastAPI
from mailkube import AsyncMailkube
@asynccontextmanager
async def lifespan(app: FastAPI):
# Created once on startup and reused for every request.
app.state.mailkube = AsyncMailkube()
try:
yield
finally:
await app.state.mailkube.aclose()
app = FastAPI(lifespan=lifespan)
@app.post("/emails")
async def send_email(to: str, subject: str, html: str):
email = await app.state.mailkube.emails.send(
from_="Acme <hello@yourdomain.com>",
to=to,
subject=subject,
html=html,
)
return {"id": email.id, "message_id": email.message_id}
from flask import Flask, jsonify, request
from mailkube import Mailkube
app = Flask(__name__)
# One shared client for the whole process.
# Reads MAILKUBE_API_KEY from the env.
client = Mailkube()
@app.post("/emails")
def send_email():
payload = request.get_json(force=True)
email = client.emails.send(
from_="Acme <hello@yourdomain.com>",
to=payload["to"],
subject=payload["subject"],
html=payload["html"],
)
return jsonify({"id": email.id, "message_id": email.message_id})
# pip install mailkube-django
# settings.py
EMAIL_BACKEND = "mailkube_django.backends.EmailBackend"
MAILKUBE_API_KEY = "mk_<key_id>_<secret>"
# anywhere in your app
from django.core.mail import send_mail
send_mail(
subject="Hello world",
message="It works!",
from_email="Acme <hello@yourdomain.com>",
recipient_list=["customer@example.com"],
)
# pip install "mailkube-django[anymail]"
# settings.py
EMAIL_BACKEND = "mailkube_django.anymail_backend.EmailBackend"
MAILKUBE_API_KEY = "mk_<key_id>_<secret>"
# Anymail's own esp_extra and tags then work as usual.
from django.core.mail import EmailMessage
message = EmailMessage(
subject="Hello world",
body="It works!",
from_email="Acme <hello@yourdomain.com>",
to=["customer@example.com"],
)
message.send()
package main
import (
"context"
"log"
"github.com/mailkube/mailkube-go"
)
func main() {
client, err := mailkube.New() // reads MAILKUBE_API_KEY
if err != nil {
log.Fatal(err)
}
email, err := client.Emails.Send(context.Background(), mailkube.SendEmailParams{
From: "Acme <hello@yourdomain.com>",
To: []string{"customer@example.com"},
Subject: "Hello world",
HTML: "<p>It works!</p>",
})
if err != nil {
log.Fatal(err)
}
log.Println(email.ID, email.MessageID)
}
type Mailer struct {
client *mailkube.Client
}
func main() {
client, err := mailkube.New() // reads MAILKUBE_API_KEY
if err != nil {
log.Fatal(err)
}
mailer := &Mailer{client: client}
r := gin.Default()
r.POST("/signup", mailer.signup)
r.Match([]string{"GET", "POST"}, "/webhooks", gin.WrapH(webhookHandler()))
log.Fatal(r.Run(":8080"))
}
client, err := mailkube.New() // reads MAILKUBE_API_KEY
if err != nil {
log.Fatal(err)
}
h := &handler{client: client}
e := echo.New()
e.Use(middleware.Recover())
e.POST("/receipts", h.sendReceipt)
e.Any("/webhooks", echo.WrapHandler(webhookHandler()))
log.Fatal(e.Start(":8080"))
client, err := mailkube.New() // reads MAILKUBE_API_KEY
if err != nil {
log.Fatal(err)
}
app := fiber.New()
app.Post("/signup", signup(client))
app.All("/webhooks", adaptor.HTTPHandler(webhookHandler()))
log.Fatal(app.Listen(":8080"))
<?php
require 'vendor/autoload.php';
use Mailkube\Client;
$client = new Client(); // reads MAILKUBE_API_KEY
$email = $client->emails->send(
from: 'Acme <hello@yourdomain.com>',
to: 'customer@example.com',
subject: 'Hello world',
html: '<p>It works!</p>',
);
echo $email->id, ' ', $email->messageId;
// composer require mailkube/mailkube-laravel
//
// config/mail.php
// 'mailers' => ['mailkube' => ['transport' => 'mailkube']],
//
// .env
// MAIL_MAILER=mailkube
// MAILKUBE_API_KEY=mk_<key_id>_<secret>
use Illuminate\Support\Facades\Mail;
Mail::to('customer@example.com')->send(new OrderShipped($order));
require "mailkube"
client = Mailkube.new # reads MAILKUBE_API_KEY
email = client.emails.send(
from: "Acme <hello@yourdomain.com>",
to: "customer@example.com",
subject: "Hello world",
html: "<p>It works!</p>"
)
puts email.id
# Gemfile
gem "mailkube-rails"
# config/environments/production.rb
config.action_mailer.delivery_method = :mailkube
config.action_mailer.mailkube_settings = {
api_key: ENV.fetch("MAILKUBE_API_KEY")
}
# Every ActionMailer delivery now goes through mailkube.
OrderMailer.with(order: order).shipped.deliver_later
import com.mailkube.MailkubeClient;
import com.mailkube.model.Email;
import com.mailkube.model.SendEmailParams;
import java.util.List;
// reads MAILKUBE_API_KEY
try (MailkubeClient client = MailkubeClient.builder().build()) {
Email email = client.emails().send(
SendEmailParams.builder(
"Acme <hello@yourdomain.com>",
List.of("customer@example.com"),
"Hello world")
.html("<p>It works!</p>")
.build());
System.out.println(email.id());
}
// build.gradle.kts
// implementation("com.mailkube:mailkube-spring-boot:X.Y.Z")
// application.properties
// mailkube.api-key=mk_<key_id>_<secret>
@Service
class OrderService {
private final MailSender mail;
OrderService(MailSender mail) {
this.mail = mail;
}
void confirm(Order order) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("Acme <hello@yourdomain.com>");
message.setTo(order.customerEmail());
message.setSubject("Your order shipped");
message.setText("It is on its way.");
mail.send(message);
}
}
curl -X POST https://api.mailkube.com/mta/v1/emails \
-H "Authorization: Bearer mk_<key_id>_<secret>" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@yourdomain.com>",
"to": "customer@example.com",
"subject": "Hello world",
"html": "<p>It works!</p>"
}'
mailkube emails send \
--from "Acme <hello@yourdomain.com>" \
--to customer@example.com \
--subject "Your invoice" \
--html "<p>Thanks for your order.</p>"
SMTP_HOST=smtp.mailkube.com
SMTP_PORT=587
SMTP_SECURITY=STARTTLS
SMTP_USERNAME=<your-smtp-credential-username>
SMTP_PASSWORD=<your-smtp-credential-password>
Everything around the send
These five sections follow one message, from the draft to the delivery report.
Templates ready to send, or your own
Copy a ready-made template, or open the editor on a blank page. Send its id and a few values, and mailkube renders the message. A wording change stops needing a deploy.
Versioned, never overwritten
Every save appends a version. Pin one, or send latest and restore an earlier snapshot when a change goes wrong.
Variables with a declared schema
Declare what a template expects. A send missing a required variable is rejected at validation.
Topics and tags on every send
Classify a send with a topic, and label it with your own tags. Both follow the message, so you can slice your sending by either one.
Topics classify the traffic
A topic is the mailing list a send belongs to: a newsletter, product updates, receipts. Set it on REST or in an SMTP header.
Tags carry your own labels
Attach up to twenty tags of your own metadata per message. They travel into the logs, the dashboards and the webhook payload.
{
"from": "Acme <hello@acme.com>",
"to": "customer@example.com",
"subject": "Your invoice",
"topic": "receipts",
"tags": [
{ "name": "campaign", "value": "billing" },
{ "name": "environment", "value": "production" }
]
}Emails that can wait
Add a timestamp to a normal send and mailkube transmits it at that instant. An appointment reminder, a nightly digest, a receipt that should not arrive at 3am.
Cancel or move it before it goes
List, retrieve, reschedule or cancel any scheduled message before its due time, one at a time or a whole batch at once.
One batch id, many sends
Give related messages the same batch id, then move or cancel the whole group in one call. mailkube is not a campaign builder; a batch is a label for one send.
| Scheduled for | Message | State |
|---|---|---|
| 07:00 +02:00 | appointment_reminder | sent |
| 08:30 +02:00 | invoice_receipt | scheduled |
| 09:00 +02:00 | nightly_digest | scheduled |
Know what happened to every email
Receive a signed event at your endpoint when a message is delivered, bounces, is opened or is marked as spam. Your application never polls.
Signed with your endpoint secret
Verify every delivery with your endpoint secret. The SDKs ship the verifier.
Retried until you answer
A failed delivery is retried with backoff for up to 48 hours. Deduplicate on the delivery id, which stays stable across retries.
{
"type": "email.delivered",
"created_at": "2026-06-30T12:00:00.123456+00:00",
"data": {
"email_id": "56761188-7520-42d8-8898-ff6fc54ce618",
"to": ["alice@example.com"],
"subject": "Sending this example",
"tags": [{ "name": "campaign", "value": "welcome_series" }],
"delivery": {
"recipient": "alice@example.com",
"timestamp": "2026-06-30T12:00:00.123456Z"
}
}
}Send from your terminal or your agent
Send mail, manage scheduled sends and stream webhook events from one binary. It carries a skill file, so a coding agent drives it correctly instead of guessing at flags.
See the message before it goes
Dry-run any send. It prints the exact request that would have gone out, and transmits nothing.
Webhook events in your terminal
Watch deliveries arrive as they happen while you build the handler. Add a forward flag to re-post each one into your local application.
$ mailkube emails send --dry-run \
--from "Acme <hello@acme.com>" \
--to customer@example.com \
--subject "Your invoice" \
--text "Thanks for your order."
{
"method": "POST",
"url": "https://api.mailkube.com/mta/v1/emails",
"body": {
"from": "Acme <hello@acme.com>",
"to": ["customer@example.com"],
"subject": "Your invoice",
"text": "Thanks for your order."
},
"dryRun": true
}
$ mailkube webhooks listen \
--public-url https://acme.tunnel.dev
listening on 127.0.0.1:4318
email.delivered 8f1d2c3b customer@example.com
$ mailkube skill install
$ claude "send Ada the welcome email"
mailkube emails send --dry-run \
--template-id welcome-v2 --var name=Ada \
--from "Acme <hello@acme.com>" \
--to ada@example.com \
--subject "Welcome aboard"
mailkube emails send \
--template-id welcome-v2 --var name=Ada \
--from "Acme <hello@acme.com>" \
--to ada@example.com \
--subject "Welcome aboard"
{
"id": "8f1d2c3b-...",
"messageId": "<8f1d2c3b-...@msg.mailkube.com>",
"to": ["ada@example.com"],
"replayed": false,
"scheduled": false
}
Pricing
Free
- 4K emails/mo
- 140 emails/day
- SMTP & API Access
- 7-day log retention
- Basic analytics
- Community
Launch
- 10K emails/mo
- 350 emails/day
- SMTP & API Access
- 7-day log retention
- Advanced analytics
- Email Support
Momentum
- 50K emails/mo
- 1700 emails/day
- SMTP & API Access
- 14-day log retention
- Advanced analytics
- Priority Support
Send your first email today
Start free, no card. Point an existing SMTP client at us, or install an SDK and make one POST.