Integrate Postal with MailKite: Four Practical Setups
For developers running Postal: integrate MailKite by moving inbound, splitting domains, adapting Postal routes, or receiving with MailKite and sending through Postal.
MX records are not a custody agreement. Publishing two providers’ MX hosts does not tell senders which addresses belong to which provider. Pick an owner for each receiving domain first; the application can choose its outbound sender independently.
Try the SDK-based receiving path without starting an SMTP server or giving an agent a live sending key:
git clone https://github.com/mailkite/demo-postal-ai-agent
cd demo-postal-ai-agent
npm ci
npm test
npm start
These are the local-run commands for the shared Postal/MailKite agent demo; server.mjs uses the MailKite SDK to verify a signed sample event and dry-run a reply when no API key is set. Leave MAILKITE_API_KEY unset for this exercise. The demo uses a stub agent, not an LLM. Its Postal contrast intentionally shows an unsafe spam-only trust decision; do not copy that as authentication policy. The demo also acknowledges before asynchronous work and uses a local demo secret. Production needs your real signing secret and durable persistence before acknowledgement. This is a payload comparison, not a production queue or a DNS migration simulator.
First, assign ownership by direction
Postal is a self-hosted mail server with SMTP and HTTP sending, inbound routes, and message history. MailKite is an inbound-first email API: it receives mail and sends parsed messages to your application as signed webhooks. Those jobs overlap at the application boundary, but their operating models differ.
Postal’s processed route payload contains useful parsed fields such as plain_body, html_body, and optional inline Base64 attachments. Its spam_status is a spam-classification result, not a normalized SPF/DKIM/DMARC verdict. MailKite’s event has its own signature, authentication metadata, and attachment references. Validate the provider-specific request, then map it into an application-owned message model.
Option 1: Move application inbound from Postal to MailKite
Retire the inbound server when its only job is feeding your application. Keep the outbound configuration if it still earns its keep. Postal isn’t an IMAP/POP mailbox host, and this migration does not transfer stored history.
The migration changes more than an MX record. Verify the MailKite domain and route, configure the webhook signing secret, update your receiver to verify signatures and use the MailKite event schema, then test bodies and attachments. Lower DNS TTL ahead of a planned cutover where possible, monitor both systems during propagation, and avoid keeping old and new MX hosts as an improvised load balancer.
The detailed walkthrough is replace Postal inbound routes with MailKite. Keep Postal running until you have confirmed real messages arrive through the new path and accounted for cached DNS.
Option 2: Keep both by splitting domains
A separate subdomain is the least disruptive starting point. Leave example.com on Postal and configure inbound.example.com for MailKite. Existing addresses stay put; the new product gets an independent receiving path.
You can migrate one product at a time without moving everyone else’s mail. The cost is bookkeeping: label provider ownership in your records so logs, retries, and attachment retrieval don’t silently use the wrong contract.
See use MailKite alongside Postal for the DNS boundary and application-level routing details.
Option 3: Send Postal routes to an app that uses MailKite
Choose this when Postal must remain the receiving server, but the application already uses MailKite’s SDK or agent workflow elsewhere. Configure Postal’s incoming route to POST processed JSON to your app, then adapt that Postal payload at your own boundary. The app can share business logic after normalization; the callback itself is not a MailKite webhook.
That distinction matters for both security and correctness. Do not run MailKite.verifyWebhook on Postal’s request or invent event.auth from spam_status. If you need aligned sender-authentication results, establish which receiving service generated them: parsing a sender-supplied Authentication-Results header does not make it trustworthy. Treat unknown provenance as unknown authentication. Decode Base64 attachment data only after size limits, and deduplicate on Postal server identity plus message ID in your durable queue.
Postal expects its endpoint to respond within five seconds. Persist the job before acknowledging and understand the retry status rules: Postal documents up to 18 attempts for retryable failures, while a 5xx fails immediately and can cause a bounce. A temporary queue failure should return a retryable non-200, non-5xx response, such as 429, rather than the habitual 503. Don’t keep an LLM call inside the request handler; Postal doesn’t care how interesting your agent’s chain of thought is.
The adapter walkthrough is connect Postal inbound routes to MailKite apps. For a runnable Postal-versus-MailKite agent example, see demo-postal-ai-agent.
Option 4: Receive with MailKite, send replies through Postal
Choose this when you want MailKite to own inbound parsing and webhook delivery, while Postal remains your outbound server. Your app verifies the MailKite event, saves it, applies sender and action policy, then calls Postal’s send API from a server-side worker.
Keep the Postal API key off the client and out of logs. Check both the HTTP response and Postal’s JSON status, and record the returned message identifiers. A timeout is ambiguous: Postal may have accepted the message before your connection dropped. Reconcile against Postal’s message history or webhooks before retrying, or a polite reply can become a very enthusiastic double reply.
This does not transfer Postal’s deliverability operations to MailKite. Postal still owns the outbound server, its reputation, DNS, upgrades, and bounce processing. The step-by-step version is receive with MailKite and reply through Postal.
Pick the setup by the thing you want to keep
| If you need… | Use this setup | Keep in mind |
|---|---|---|
| Application inbound without operating Postal for that domain | Replace Postal inbound | Update DNS, webhook verification, payload handling, and attachment code |
| Postal’s current workflows plus MailKite for another product | Split domains or subdomains | One MX owner per domain; label provider ownership in your app |
| Postal to remain the receiver while your app standardizes internally | Postal route adapter | Postal callbacks are not MailKite-signed and do not carry its normalized auth block |
| Managed inbound with Postal outbound | MailKite receive, Postal send | Keep API credentials server-side and reconcile ambiguous send timeouts |
If you prefer to operate the whole stack, Postal is a reasonable choice: it offers control over the server and message storage. If you want Postal’s route to keep delivering to your app, keep it. If the job is simply receiving application email without operating an inbound mail server, MailKite (which we build) is designed for that boundary. Postmark inbound, SES plus Lambda, and a self-hosted Haraka setup are also worth evaluating against your data, control, and operations requirements.
Run the code comparison before changing DNS. A successful local webhook test proves your handler works; it says nothing about MX propagation, domain verification, or outbound reputation. Test those separately.
Sources
- Postal, Receiving e-mail by HTTP, retrieved 2026-10-26.
- Postal, Routing incoming e-mail, retrieved 2026-10-26.
- Postal, Using the API, retrieved 2026-10-26.
- MailKite, Receiving documentation, retrieved 2026-10-26.
- MailKite, Webhook security, retrieved 2026-10-26.