> ## 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.opened

> Mail a workflow or sequence sent was opened, as reported by its tracking pixel.

Fires when the tracking pixel in an email sent by a workflow or a sequence is
loaded. The pixel is added by the **Send email** step when tracking is on, which
is the default.

## Payload

```json theme={null}
{
  "id": "whd_3Fn8kR2vB",
  "event": "email.opened",
  "createdAt": "2026-09-11T10:12:47.390Z",
  "data": {
    "messageId": "snd_2Wq9rT4bLx",
    "recordId": "rec_3Ab9xK2mQ7"
  }
}
```

<ResponseField name="messageId" type="string" required>
  Custral's tracking id for the send (`snd_…`). One send carries the same id on
  all of its opens and clicks. Always present.
</ResponseField>

<ResponseField name="recordId" type="string">
  The record the workflow or sequence run was about. Absent when the send was
  not tied to a record.
</ResponseField>

## `messageId` is a tracking id

Custral mints the id when the step sends the email, and it identifies that one
send across its opens and its [clicks](/dev/webhooks/events/email-clicked). It
does **not** match the mail provider's message id that
[`email.sent`](/dev/webhooks/events/email-sent) carries, so the two cannot be
joined on it.

## Worth knowing

* **An open is a loaded image.** Mail clients and privacy proxies that fetch
  images automatically count as opens, sometimes before anyone has looked at
  the message.
* **Every load fires.** Opening the same email three times delivers three
  events. Deduplicate on `messageId` if you only want the first open.
* **Blocked images mean no event.** A recipient whose mail client does not load
  images never fires it, even after reading the email.
* **Only automated sends are tracked this way.** Mail sent from the inbox
  composer shows read receipts in the app and does not fire this event.

## Example

```ts theme={null}
custral.on("email.opened", async (event) => {
  const {messageId, recordId} = event.data;
  if (!recordId) return;

  // Count the first open per send only.
  const firstOpen = await cache.add(`opened:${messageId}`); // false if already present
  if (!firstOpen) return;

  await scoring.bump({recordId: String(recordId), signal: "email_opened"});
});
```


## Related topics

- [Email](/dev/webhooks/events/email.md)
- [email.sent](/dev/webhooks/events/email-sent.md)
- [Event catalog](/dev/webhooks/events/overview.md)
- [Webhooks](/dev/webhooks/overview.md)
- [email.clicked](/dev/webhooks/events/email-clicked.md)
