MailKite
Get your API key
Receiving email

Receive Gmail emails live

Forward new Gmail messages to a MailKite receiving address and receive signed JSON webhooks in your application, without mailbox polling. Here, live means event-driven delivery after the email arrives, with normal SMTP and forwarding delays; it does not promise an exact latency.

1. Prepare a receiving address

In MailKite, add a domain or claim an available hosted subdomain. Complete the verification shown in the dashboard before receiving. For your own domain, publish the exact MX record MailKite returns. Choose an address such as inbound@your-receiving-domain.

Keep Gmail or iCloud as the host of your existing mailbox. The MailKite address is a separate forwarding destination, so you do not change the original mailbox's MX records.

For a detailed setup walkthrough, use the quickstart and domain verification docs. Your HTTP receiver needs a public HTTPS URL; a development tunnel can expose local port 3000 during testing.

The shared demo repository contains the exact receiver sources, local checks and inbox inspector used by this series.

2. Configure the webhook and run a receiver

Create a webhook route matching your receiving address and point it to https://your-app.example/hooks/mailkite, replacing the example host with your public HTTPS host. Copy that route's signing secret into the receiver's environment. Avoid a catch-all route if you only want mail for one address.

Python 3.10+; SQLite is included in Python's standard library. Set MAILKITE_WEBHOOK_SECRET to the signing secret for your configured MailKite webhook, supplied through your environment or secret manager. You do not need an API key for local signature verification.

The Python receiver below works for either mailbox. If your application uses another language, pick a language-specific receiving guide instead.

install dependencies
python3 -m venv .venv
source .venv/bin/activate
python -m pip install mailkite-dev flask

The PyPI distribution is mailkite-dev, while the import is mailkite. Flask's request.get_data() retains the raw bytes. The SQLite context manager commits the insert before the response, and the primary key makes repeated deliveries safe to store.

receive.py
# Docs: docs/website/content/live-receiving-email-series.md
import json
import os
import sqlite3
from flask import Flask, request
from mailkite import verify_webhook

app = Flask(__name__)
SECRET = os.environ["MAILKITE_WEBHOOK_SECRET"]
DB = os.environ.get("INBOX_DB", "inbox.sqlite")
with sqlite3.connect(DB) as db:
db.execute("CREATE TABLE IF NOT EXISTS inbox (id TEXT PRIMARY KEY, payload TEXT NOT NULL)")

@app.post("/hooks/mailkite")
def receive():
raw = request.get_data()
signature = request.headers.get("x-mailkite-signature", "")
if not verify_webhook(signature, raw, SECRET):
return "bad signature", 401
try:
event = json.loads(raw)
except (ValueError, UnicodeDecodeError):
return "bad JSON", 400
if not isinstance(event, dict):
return "bad event", 400
if event.get("type") != "email.received":
return {"status": "ok"}
if not isinstance(event.get("id"), str) or not event["id"]:
return "missing id", 400
try:
with sqlite3.connect(DB, timeout=5) as db:
db.execute("INSERT INTO inbox (id, payload) VALUES (?, ?) ON CONFLICT(id) DO NOTHING",
(event["id"], raw.decode("utf-8")))
except sqlite3.Error:
return "storage unavailable", 503
return {"status": "ok"} # The transaction has committed before this response.

if __name__ == "__main__":
app.run(port=int(os.environ.get("PORT", "3000")))

Download the exact receiver shown above. Save it beside its dependencies, then start it with the environment variable already set:

run locally
python receive.py

This example stores each event once in a local SQLite inbox before returning success. Use persistent disk; an ephemeral deployment can delete that inbox on restart. A separate worker should process stored events and track completion. Storage deduplication alone does not make external side effects exactly-once.

3. Test the exact receiver and address

Download the local signed test and run it with Node.js and the same webhook secret. It creates synthetic HTTP requests only, including a duplicate id and a tampered body; it sends no email. The first two requests should return the SDK's JSON acknowledgement, and the tampered one should return 401.

test the local receiver
node check.mjs http://127.0.0.1:3000/hooks/mailkite

Then send a real message to the exact receiving address to test routing. The dashboard's domain-webhook test uses a catch-all route; it does not test an exact-address route. See signature verification and delivery and retries.

Download the local inbox inspector to read stored message bodies, including Gmail's confirmation message. This displays mailbox contents in your own terminal; keep it off the public web.

inspect accepted messages
python3 inspect-inbox.py inbox.sqlite

4. Enable Gmail forwarding

  1. Open Gmail on a computer and choose Settings → See all settings.
  2. Open Forwarding and POP/IMAP (or Forwarding) and choose Add a forwarding address.
  3. Enter your working MailKite receiving address. Gmail sends a verification message there.
  4. Read that message in MailKite's inbox or in the receiver's stored payload. Follow Google's confirmation link yourself.
  5. Return to Gmail, refresh settings, select Forward a copy of incoming mail to, choose the destination, and keep Gmail's copy in the inbox.
  6. Choose Save Changes. Send a new external email to Gmail and check its webhook delivery.

Google forwards new messages except spam. To forward only selected messages, leave blanket forwarding disabled and create a filter with Forward it after verifying the address. A Workspace administrator may restrict external forwarding.

Use Google's automatic forwarding instructions if the settings labels change. Forwarding does not import your old mailbox or synchronize labels and read state.

Receiving a forwarded message does not authorize sending as your Gmail or iCloud address. Reply through the original provider when that identity must be preserved, or send from a separately verified MailKite domain.

5. Check a real message and its payload

Send a new message to your original mailbox. Confirm delivery in MailKite and receipt at the endpoint. The JSON event has type: "email.received", a stable id, sender and recipient address objects, decoded bodies, attachment metadata and authentication results.

  • Body: use text, falling back to textFromHtml when the plain-text part is null.
  • Attachments: process the signed download URLs after durable acceptance, before they expire.
  • Authentication: the webhook signature authenticates MailKite's delivery. It does not prove the original sender is trustworthy. Forwarding can change email-authentication results.
  • Retries: replay a delivery and confirm your production handler does not create a second job for the same message id.

Troubleshoot the delivery boundary

SymptomCheck
No message in MailKiteReceiving-domain verification, MX records, the destination address, and the provider's forwarding settings.
Message stored, no webhookThe route's exact address match and enabled webhook destination.
401 responseThe signing secret, raw body, signature header and server clock. Do not disable freshness checks to hide the problem.
503 responseDatabase availability, persistent storage and write permissions. Replay after correcting the storage failure.
Duplicate application workUnique message ids, queue deduplication and downstream side-effect idempotency.

Continue with the complete event reference, other receiving guides or the companion implementation article.