Inbound mail over the webhook
IMAP polling is the default because it needs no public ingress. The webhook is the production path: sub-second delivery, and the provider retries and buffers for you. It needs a URL your provider can reach.
Everything past the route is the same pipeline IMAP feeds — parse, quarantine, requester capture, company filing, attachment storage. Switching mode changes how bytes arrive and nothing else.
Setting it up
- Settings → Email → Inbound, set mode to Webhook and pick your provider.
- Save. Kiku generates the signing secret on that first save and shows it, along with the
endpoint URL (
https://your-instance/api/email/inbound/<organization-id>). - Point the provider’s inbound route at the endpoint, carrying the secret.
The secret goes in either:
Authorization: Bearer <secret>— for a provider that can set headers, i.e. one running your own code, like a Cloudflare Email Worker.?secret=<secret>on the URL — for a provider whose whole configuration surface is one URL field. Postmark, Mailgun and SNS forwarders all fall in here.
Without a valid secret the route answers 401, never 403: Postmark stops retrying for good on a 403, so a rotated secret has to fail in a way that redelivers.
While the instance is in IMAP mode the route answers 503. Set the mode first.
Providers
Postmark
Turn on “Include raw email content in JSON payload” on the Postmark server’s inbound
settings. Without it the payload has no RawEmail field and the route answers 400 saying so.
Inbound is a Pro-plan feature.
Mailgun
The route URL registered with Mailgun has to end in /mime so Mailgun posts the raw
message in a body-mime field rather than its own parsed fields. Both multipart/form-data
and application/x-www-form-urlencoded posts are read. Mailgun caps its webhook at 25 MB.
Amazon SES
The SNS action is not supported. It caps the entire email at 150 KB, which is unusable for real support mail; there is no truncated mode, so Kiku does not pretend to offer one.
Use the S3 receipt action (40 MB) and a small function that reads the object and posts it:
{ "content": "<the S3 object, raw RFC822 or base64>" }An SNS envelope wrapping that same object — { "Type": "Notification", "Message": "…" } — is
unwrapped too, so a Lambda may forward the notification verbatim once it has swapped the S3
pointer for the object’s bytes. A notification carrying only the pointer is quarantined with
that explanation, because Kiku holds no AWS credentials and cannot fetch the object itself.
Note also that SES matches receipt rules on the envelope RCPT TO, not the To: header, and
that SES inbound is only offered in some regions.
Cloudflare Email Workers
Post message.raw straight through — the worker’s stream is already RFC822, which is the
shape every other shim decodes to.
export default { async email(message, env) { await fetch(env.KIKU_ENDPOINT, { method: 'POST', headers: { authorization: `Bearer ${env.KIKU_SECRET}`, 'content-type': 'message/rfc822' }, body: message.raw, }) },}Raw
Anything else that can POST the RFC822 bytes as the request body, including curl for a
replay by hand.
Size and failure
- The 25 MB cap holds here exactly as it does on IMAP, and is not configurable. It is measured on the decoded message, not the posted body, since an envelope that base64s the message is a third larger than what is inside it. Over the cap: 413, and a row in quarantine.
- An envelope Kiku cannot open — the wrong provider selected, a setting not turned on — is 400, and the posted bytes are kept in quarantine so a corrected setting can replay them.
- A message that decodes but will not parse goes to quarantine the same way it does on the IMAP path, with its raw bytes retained.
- The same bytes posted twice to one organization produce one ticket: the content hash, prefixed with the organization id, is the ingestion workflow’s idempotency key, so a provider retry after a timeout costs nothing without deduplicating another organization’s message.
- Nothing records a last-successful-fetch stamp on this path. Nothing polls, so there would be nothing to stamp; the provider’s own delivery log is the health surface.