Receive email

Process inbound email

Lettermint sends each queued inbound message as a signed message.inbound webhook. A message at or above the route spam threshold stays quarantined and does not reach a webhook until an authorized user processes it.

A safe endpoint completes these actions:

  1. Verify the signature against the raw request body.
  2. Record the webhook delivery ID.
  3. Save the event or add it to a queue.
  4. Return a 2xx response.

Create the webhook

Open the inbound route

  1. In the dashboard, select Projects.
  2. Select your project and open Routes.
  3. Select the inbound route.

Add the endpoint

  1. Select Webhooks.
  2. Select Create webhook.
  3. Enter the public HTTPS URL for your endpoint.

Lettermint automatically subscribes the webhook to message.inbound.

Copy the signing secret after you create the webhook. Store the secret in your application secret manager.

Test a real message

Send an email to the route address from Settings, such as test@k7x2m9qpfa.lettermint.app. Any name before the @ works. Open the webhook delivery log.

Check the request, response status, and retry attempts.

Verify and queue the event

Verify X-Lettermint-Signature against the unmodified request body before you parse the JSON. Follow Signed webhooks for complete framework examples.

The following handlers queue the verified event and return a response:

app.post('/webhooks/lettermint', verifyLettermintSignature, async (req, res) => { const event = req.body if (event.event !== 'message.inbound') { return res.sendStatus(204) } await inboundQueue.add('process-inbound', event.data, { // Let the queue reject a repeated delivery of the same webhook. jobId: event.id, }) return res.sendStatus(204) })

Prevent duplicate processing

Lettermint uses the same top-level id for each retry of one webhook delivery. Store this ID with a unique database constraint. You can also use the duplicate prevention feature in your queue.

Always verify the signature before you trust the event. Use the exact raw request body. A parsed and serialized body has different bytes.

Use the right address field

SMTP envelope data and visible message headers have different purposes. Use the envelope recipient to route a message. Use header addresses for display.

FieldUse
idIdentify one webhook delivery. Lettermint uses it again for retries.
data.message_idIdentify the inbound message and its stored content.
timestampRead the time that Lettermint created the webhook event.
data.dateRead the time that Lettermint received the inbound message.
data.recipientRoute the message by its SMTP envelope recipient.
data.to and data.ccDisplay the parsed To and Cc headers.
data.envelope.mail_fromCheck the SMTP envelope sender for delivery problems or bounces.
data.fromDisplay the parsed From header.
data.reply_toSelect the parsed reply address. This field can be null.

Forwarding, aliases, and blind copies can change the envelope recipient. Therefore, it can differ from To or Cc.

Do not route a tenant or workflow from a visible header.

For a user reply, use reply_to if it has a value. Otherwise, use from.email.

Treat both fields as untrusted input. Do not send an automatic reply only because an address is in the payload.

Process content safely

  • Sanitize data.body.html before you display it. Email HTML can contain scripts, tracking resources, and false links.
  • Use data.body.text if you do not need rich formatting. Each body field can be null.
  • Do not log message bodies, authentication headers, attachment content, or signed URLs.
  • Check spam and authentication results before you start a sensitive action.
  • Follow the attachment and raw email guide before you store files.

Return a response

Return a 2xx response after you save or queue the event. Do not wait for other processing to finish.

Lettermint retries a delivery after a timeout or a response outside the 2xx range. Your endpoint can receive the same event more than once. See Delivery and retries for the retry schedule.

For the complete event shape, field types, and nullable values, see the message.inbound webhook reference.

Troubleshooting

Why is there no webhook delivery?
  • Confirm that the webhook belongs to the inbound route.
  • Confirm that the endpoint is available through public HTTPS.
  • For a route domain, confirm that the route ID part of the address is exact. The name before the @ can be anything.
  • For the compatibility address, confirm that the recipient is an exact match and has no +tag.
  • For a custom domain, confirm that the domain is verified.
Why does signature verification fail?

Capture the raw request body before the JSON parser runs. Use the signing secret for this webhook.

Do not use an API token or a secret from a different webhook.

Why did my endpoint receive the event more than once?

Lettermint retries after a timeout or a response outside the 2xx range. Store the top-level id with a unique constraint.

Make sure that later operations can safely receive the same event again.

Why did the wrong workflow receive the message?

Route the message with data.recipient. For plus addressing, also use data.subaddress.

Do not use the first visible To address as the delivery destination.