Route support@yourapp.com to a signed webhook and create a ticket the instant mail lands — sender, subject, body, and threadId already parsed. Reply through the same API and it threads correctly in the customer's inbox.
The parsed message — from, subject, text/HTML, threadId, attachments — arrives as JSON, so creating a ticket is a few field reads, not a MIME parser.
Send your reply through the Send API with the message-id and In-Reply-To/References are handled — the customer sees one clean thread.
Webhooks retry with backoff, and you can replay any message in one click if your app was down — no support email lost to a 500.
Every event carries SPF/DKIM/DMARC and spam results, so you can flag or drop forged 'urgent' support mail before it becomes a ticket.
Add MX (or use a managed subdomain) and route the support@ address — or a catch-all — to your webhook URL in the dashboard.
Read event.from.address, event.subject, event.text, and event.threadId in your handler and upsert a ticket.
Send the agent's reply through /v1/send referencing the thread — it lands back in the customer's inbox, correctly threaded.
Verify the HMAC signature, then a ticket is a handful of field reads — no MIME parsing, no polling.
import { MailKite } from "mailkite";
app.post("/hooks/mailkite", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-mailkite-signature"];
if (!MailKite.verifyWebhook(sig, req.body, process.env.MAILKITE_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const evt = JSON.parse(req.body.toString("utf8"));
tickets.create({
from: evt.from.address,
subject: evt.subject,
body: evt.text,
thread: evt.threadId, // reply to this to thread correctly
spoofed: evt.auth.dkim !== "pass",
});
res.type("application/json").send(MailKite.replyOk());
});from flask import request, abort
from mailkite import verify_webhook, reply_ok
@app.post("/hooks/mailkite")
def hook():
sig = request.headers.get("x-mailkite-signature", "")
if not verify_webhook(sig, request.get_data(), os.environ["MAILKITE_WEBHOOK_SECRET"]):
abort(401)
evt = request.get_json()
tickets.create(
from_=evt["from"]["address"],
subject=evt["subject"],
body=evt["text"],
thread=evt["threadId"], # reply to this to thread correctly
spoofed=evt["auth"]["dkim"] != "pass",
)
return reply_ok(), 200, {"content-type": "application/json"}<?php
$mk = new \MailKite\Client(""); // no API key needed to verify
$raw = file_get_contents("php://input");
$sig = $_SERVER["HTTP_X_MAILKITE_SIGNATURE"] ?? "";
if (!$mk->verifyWebhook($sig, $raw, getenv("MAILKITE_WEBHOOK_SECRET"))) {
http_response_code(401);
exit;
}
$evt = json_decode($raw, true);
Tickets::create([
'from' => $evt['from']['address'],
'subject' => $evt['subject'],
'body' => $evt['text'],
'thread' => $evt['threadId'], // reply to this to thread correctly
'spoofed' => $evt['auth']['dkim'] !== 'pass',
]);
header("content-type: application/json");
echo $mk->replyOk();// Spring Boot
@PostMapping(value = "/hooks/mailkite", produces = "application/json")
public ResponseEntity<String> mailkite(
@RequestHeader("x-mailkite-signature") String signature,
@RequestBody byte[] rawBody) throws Exception {
MailKite mk = new MailKite(""); // no API key needed to verify
String body = new String(rawBody, StandardCharsets.UTF_8);
if (!mk.verifyWebhook(signature, body, System.getenv("MAILKITE_WEBHOOK_SECRET"))) {
return ResponseEntity.status(401).build();
}
Map<String, Object> evt = new ObjectMapper().readValue(body, new TypeReference<>() {});
tickets.create(evt); // from.address, subject, text, threadId, auth.dkim
return ResponseEntity.ok(mk.replyOk());
}http.HandleFunc("/hooks/mailkite", func(w http.ResponseWriter, r *http.Request) {
rawBody, _ := io.ReadAll(r.Body)
sig := r.Header.Get("x-mailkite-signature")
if !mailkite.VerifyWebhook(sig, string(rawBody), secret) {
w.WriteHeader(http.StatusUnauthorized)
return
}
var evt map[string]any
json.Unmarshal(rawBody, &evt)
tickets.Create(evt) // from.address, subject, text, threadId, auth.dkim
w.Header().Set("content-type", "application/json")
w.Write([]byte(mailkite.ReplyOk()))
})require "sinatra"
require "json"
require "mailkite"
post "/hooks/mailkite" do
raw = request.body.read
sig = request.env["HTTP_X_MAILKITE_SIGNATURE"]
halt 401 unless Mailkite.verify_webhook(sig, raw, ENV["MAILKITE_WEBHOOK_SECRET"])
evt = JSON.parse(raw)
Tickets.create(
from: evt["from"]["address"],
subject: evt["subject"],
body: evt["text"],
thread: evt["threadId"], # reply to this to thread correctly
spoofed: evt["auth"]["dkim"] != "pass"
)
content_type :json
Mailkite.reply_ok
end POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
}POST /your-webhook Content-Type: application/json
x-mailkite-signature: t=…,v1=… (HMAC-SHA256 — verify locally)
{
"id": "msg_2Hk9…",
"type": "email.received",
"from": { "address": "ada@example.com", "name": "Ada Lovelace" },
"to": [{ "address": "support@myapp.ai", "name": "Support" }],
"subject": "Re: invoice #1042",
"text": "Looks good — approved!",
"html": "<p>Looks good — approved!</p>",
"threadId": "<a1b2c3@mail.example.com>",
"receivedAt": 1785196800000,
"receivedAtIso": "2026-07-28T00:00:00.000Z",
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
"attachments": [
{
"id": "msg_2Hk9…:0",
"filename": "po.pdf",
"contentType": "application/pdf",
"size": 18213,
"url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…"
}
]
} No — you decide what a 'ticket' is. The webhook hands you the parsed message; create a row in your database, open an issue, page someone, whatever fits. Many teams wire it into their existing helpdesk.
Send your reply through the Send API referencing the original message; MailKite sets In-Reply-To/References so the customer's mail client shows a single conversation.
Deliveries retry automatically with backoff, and every message is replayable in one click from the dashboard on paid plans, so nothing is lost while you recover.
Start free on unlimited domains — no credit card. Or browse the other solutions.