> ## Documentation Index
> Fetch the complete documentation index at: https://docs.custral.com/llms.txt
> Use this file to discover all available pages before exploring further.

# email.received

> A message from outside the workspace arrived in a connected mailbox, in a thread that is a conversation.

Fires when a connected mailbox receives a message from outside the workspace and
the message's thread is a conversation in Custral.

<Note>
  Every message that fires `email.received` also fires
  [`email.replied`](/dev/webhooks/events/email-replied), with the same payload.
  Subscribe to one of the two, or each message is handled twice.
</Note>

## Payload

```json theme={null}
{
  "id": "whd_5Rt8mK2pW",
  "event": "email.received",
  "createdAt": "2026-09-11T14:02:37.118Z",
  "data": {
    "messageId": "18f2d3e7a9b1c4f6",
    "threadId": "18f2d1c04b7e9a23",
    "channelId": "chan_4Kp8wN",
    "conversationId": "conv_3Hq9rT",
    "from": "ada@ravennafoods.com",
    "fromName": "Ada Lovell",
    "to": ["sales@acme.com"],
    "cc": [],
    "subject": "Re: Pricing for the Ravenna rollout",
    "receivedAt": "2026-09-11T14:02:31.000Z",
    "folder": "INBOX"
  }
}
```

<ResponseField name="messageId" type="string" required>
  The mail provider's id for the message. Always present.
</ResponseField>

<ResponseField name="threadId" type="string">
  The mail provider's id for the thread.
</ResponseField>

<ResponseField name="channelId" type="string">
  The connected mailbox the message arrived in.
</ResponseField>

<ResponseField name="conversationId" type="string">
  The conversation the thread belongs to. Present on every delivery, because
  this event only fires for a thread that is a conversation.
</ResponseField>

<ResponseField name="from" type="string">
  The sender's address.
</ResponseField>

<ResponseField name="fromName" type="string">
  The sender's display name. **Omitted** when the message carries none.
</ResponseField>

<ResponseField name="to" type="string[]">
  The addresses the message was sent to.
</ResponseField>

<ResponseField name="cc" type="string[]">
  The addresses copied. An empty array when nobody was copied.
</ResponseField>

<ResponseField name="subject" type="string">
  The subject line. An empty string when the message has none.
</ResponseField>

<ResponseField name="receivedAt" type="string">
  The message's date, as an ISO 8601 timestamp.
</ResponseField>

<ResponseField name="folder" type="string">
  The provider's id for the first folder or label the message is filed under. On
  a Microsoft mailbox this is an opaque id rather than a name. **Omitted** when
  the provider lists none.
</ResponseField>

<ResponseField name="recordIds" type="string[]">
  Records the message is linked to. Mail from a connected mailbox does not carry
  them today, so this is omitted; look the conversation up by `conversationId`
  to see who it is with.
</ResponseField>

## When it fires

* **The sender is outside the workspace.** A message sent from any of the
  workspace's connected mailboxes, a teammate's included, delivers
  [`email.sent`](/dev/webhooks/events/email-sent) instead.
* **The thread is a conversation.** A reply in a thread that is already a
  conversation counts. A new thread counts when the **New mail handling**
  setting, or the mailbox's own override of it, turns it into one. Mail left in
  the inbox sends nothing.
* **The message is not in a spam folder.** A message the provider files as spam
  or junk sends no email event at all.

## What sends nothing

* **A blocked sender.** Mail from a sender blocked on that mailbox is moved to
  the trash.
* **A repeat of the same message.** Custral skips a message it has processed in
  the last five minutes. That includes a copy arriving in a second connected
  mailbox of the workspace, so the message is delivered once, with the
  `channelId` of whichever mailbox was processed first.

## Worth knowing

* **No body.** The payload carries the subject and the addressing. Read the
  message itself from the conversation through the
  [Conversations API](/dev/api-reference/conversations).
* **Ids belong to one mailbox.** Each connected mailbox has its own `messageId`
  and `threadId` for the same mail. Key on `conversationId` to follow a
  conversation.

## Example

```ts theme={null}
const KEY_ACCOUNTS = new Set(["ravennafoods.com"]);

custral.on("email.received", async (event) => {
  const {from, subject, conversationId} = event.data;
  const domain = String(from).split("@")[1]?.toLowerCase();
  if (!domain || !KEY_ACCOUNTS.has(domain)) return;

  await alerts.notify({team: "accounts", text: `${from}: ${subject}`, conversationId});
});
```


## Related topics

- [Email](/dev/webhooks/events/email.md)
- [email.replied](/dev/webhooks/events/email-replied.md)
- [Event catalog](/dev/webhooks/events/overview.md)
- [Webhooks](/dev/webhooks/overview.md)
- [Workflow Triggers](/automation/workflows/triggers.md)
