Skip to main content

messageUpdated

The messageUpdated webhook event is triggered when EmailEngine detects that the flags or labels on a message have changed. This event enables real-time synchronization of message state changes with external systems.

When This Event is Triggered

The messageUpdated event fires when:

  • A message is marked as read (\Seen flag added)
  • A message is marked as unread (\Seen flag removed)
  • A message is flagged/starred (\Flagged flag added)
  • A message is unflagged (\Flagged flag removed)
  • A message is replied to (\Answered flag added)
  • A draft flag is changed (\Draft flag added/removed)
  • Custom IMAP flags are added or removed
  • Gmail labels are added or removed (Gmail API accounts only)

The event is triggered after EmailEngine confirms the flag/label change on the mail server.

Common Use Cases

  • Read status synchronization - Track when users read emails across devices
  • Priority tracking - Monitor flagged/starred message changes
  • CRM integration - Update ticket status when emails are marked as handled
  • Analytics - Track user engagement patterns and response times
  • Label-based workflow - Trigger actions based on Gmail label changes
  • Archival systems - Update message metadata when flags change
  • Notification systems - Alert when important messages are flagged

Payload Schema

Top-Level Fields

FieldTypeRequiredDescription
serviceUrlstringNoThe configured EmailEngine service URL
accountstringYesAccount ID where the message was updated
datestringYesISO 8601 timestamp when the webhook was generated
pathstringYesMailbox folder path (e.g., "INBOX", "Sent Mail")
specialUsestringNoSpecial use flag of the folder (e.g., "\Inbox", "\Sent")
eventstringYesEvent type, always "messageUpdated" for this event
eventIdstringYesUnique identifier for this webhook delivery
dataobjectYesMessage update data (see below)

Message Data Fields (data object)

All Account Types

FieldTypeRequiredDescription
idstringYesEmailEngine's unique message ID (base64url encoded for IMAP, Gmail ID for Gmail API)
uidnumberIMAP onlyIMAP UID of the message within the folder
threadIdstringGmail onlyGmail thread ID the message belongs to
changesobjectYesObject describing what changed (see below)

Changes Object Structure

The changes object contains flag and/or label changes:

Flag Changes (changes.flags)

FieldTypeDescription
addedarrayArray of flags that were added (e.g., ["\Seen"])
deletedarrayArray of flags that were removed (e.g., ["\Flagged"])
valuearrayComplete current flag list after the change

Label Changes (changes.labels) - Gmail Only

FieldTypeDescription
addedarrayArray of labels that were added (e.g., ["IMPORTANT"])
deletedarrayArray of labels that were removed (e.g., ["INBOX"])
valuearrayComplete current label list after the change

Common IMAP Flags

FlagDescription
\SeenMessage has been read
\AnsweredMessage has been replied to
\FlaggedMessage is flagged/starred
\DeletedMessage is marked for deletion
\DraftMessage is a draft

Common Gmail Labels

LabelDescription
INBOXMessage is in inbox
SENTMessage is in sent folder
DRAFTMessage is a draft
TRASHMessage is in trash
SPAMMessage is marked as spam
STARREDMessage is starred
IMPORTANTMessage is marked important
UNREADMessage is unread

Example Payloads

IMAP Account - Message Marked as Read

{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T06:43:46.195Z",
"path": "INBOX",
"specialUse": "\Inbox",
"event": "messageUpdated",
"eventId": "c36ab19a-58cc-4372-a567-0e02b2c3d479",
"data": {
"id": "AAAADAAABy4",
"uid": 1838,
"changes": {
"flags": {
"added": ["\Seen"],
"value": ["\Seen"]
}
}
}
}

IMAP Account - Message Flagged and Read

{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T07:15:22.456Z",
"path": "INBOX",
"specialUse": "\Inbox",
"event": "messageUpdated",
"eventId": "d47bc20a-69dd-4483-b678-1f13c3d4e590",
"data": {
"id": "AAAADAAABy8",
"uid": 1845,
"changes": {
"flags": {
"added": ["\Seen", "\Flagged"],
"value": ["\Seen", "\Flagged"]
}
}
}
}

IMAP Account - Message Unflagged

{
"serviceUrl": "https://emailengine.example.com",
"account": "user123",
"date": "2025-10-17T08:30:15.789Z",
"path": "INBOX",
"specialUse": "\Inbox",
"event": "messageUpdated",
"eventId": "e58cd31b-7aee-5594-c789-2g24d4e5f6a1",
"data": {
"id": "AAAADAAABy8",
"uid": 1845,
"changes": {
"flags": {
"deleted": ["\Flagged"],
"value": ["\Seen"]
}
}
}
}

Gmail API Account - Label Added

{
"serviceUrl": "https://emailengine.example.com",
"account": "gmail-user",
"date": "2025-10-17T09:45:30.123Z",
"path": "[Gmail]/All Mail",
"event": "messageUpdated",
"eventId": "f69de42c-8bff-6605-d890-3h35e5f6g7b2",
"data": {
"id": "18b5c7d8e9f01234",
"threadId": "18b5c7d8e9f01234",
"changes": {
"labels": {
"added": ["IMPORTANT"],
"value": ["INBOX", "IMPORTANT", "UNREAD"]
}
}
}
}

Gmail API Account - Multiple Changes

{
"serviceUrl": "https://emailengine.example.com",
"account": "gmail-user",
"date": "2025-10-17T10:00:00.000Z",
"path": "[Gmail]/All Mail",
"event": "messageUpdated",
"eventId": "a70ef53d-9c00-7716-e9a1-4i46f6g7h8c3",
"data": {
"id": "18b5c7d8e9f01234",
"threadId": "18b5c7d8e9f01234",
"changes": {
"flags": {
"added": ["\Seen"],
"value": ["\Seen", "\Flagged"]
},
"labels": {
"added": ["STARRED"],
"deleted": ["UNREAD"],
"value": ["INBOX", "IMPORTANT", "STARRED"]
}
}
}
}

Microsoft Outlook Account - Message Read

{
"serviceUrl": "https://emailengine.example.com",
"account": "outlook-user",
"date": "2025-10-17T11:20:45.678Z",
"path": "Inbox",
"event": "messageUpdated",
"eventId": "b81fg64e-ad11-8827-fa2b-5j57g7h8i9d4",
"data": {
"id": "AAMkADI2NGVhZTVlLTI1OGItNDUwZS05ZDVkLWQzN2E2MDUyYzc3YQBGAAAAAAI",
"changes": {
"flags": {
"added": ["\Seen"],
"value": ["\Seen"]
}
}
}
}

Handling the Event

Basic Handler

async function handleMessageUpdated(event) {
const { account, path, data } = event;
const { changes } = data;

console.log(`Message ${data.id} updated in ${account}:`);

if (changes.flags) {
if (changes.flags.added?.length) {
console.log(` Flags added: ${changes.flags.added.join(', ')}`);
}
if (changes.flags.deleted?.length) {
console.log(` Flags removed: ${changes.flags.deleted.join(', ')}`);
}
console.log(` Current flags: ${changes.flags.value.join(', ')}`);
}

if (changes.labels) {
if (changes.labels.added?.length) {
console.log(` Labels added: ${changes.labels.added.join(', ')}`);
}
if (changes.labels.deleted?.length) {
console.log(` Labels removed: ${changes.labels.deleted.join(', ')}`);
}
console.log(` Current labels: ${changes.labels.value.join(', ')}`);
}
}

Track Read Status

async function handleMessageUpdated(event) {
const { account, data } = event;
const { changes } = data;

// Check if message was marked as read
if (changes.flags?.added?.includes('\Seen')) {
await db.messages.update({
where: {
accountId: account,
messageId: data.id
},
data: {
isRead: true,
readAt: new Date()
}
});
console.log(`Message ${data.id} marked as read`);
}

// Check if message was marked as unread
if (changes.flags?.deleted?.includes('\Seen')) {
await db.messages.update({
where: {
accountId: account,
messageId: data.id
},
data: {
isRead: false,
readAt: null
}
});
console.log(`Message ${data.id} marked as unread`);
}
}

Track Flagged Messages

async function handleMessageUpdated(event) {
const { account, data } = event;
const { changes } = data;

const isFlagged = changes.flags?.value?.includes('\Flagged');

if (changes.flags?.added?.includes('\Flagged')) {
// Message was flagged
await notifyPriorityMessage(account, data.id);
await db.messages.update({
where: { accountId: account, messageId: data.id },
data: { isFlagged: true, flaggedAt: new Date() }
});
}

if (changes.flags?.deleted?.includes('\Flagged')) {
// Message was unflagged
await db.messages.update({
where: { accountId: account, messageId: data.id },
data: { isFlagged: false, flaggedAt: null }
});
}
}

Gmail Label-Based Workflow

async function handleMessageUpdated(event) {
const { account, data } = event;
const { changes } = data;

if (!changes.labels) return;

// Trigger workflow when message is labeled "Work/Urgent"
if (changes.labels.added?.includes('Work/Urgent')) {
await triggerUrgentWorkflow(account, data.id);
}

// Archive action when removed from INBOX
if (changes.labels.deleted?.includes('INBOX')) {
await markAsArchived(account, data.id);
}

// Move to CRM when "Customer" label is added
if (changes.labels.added?.includes('Customer')) {
await syncToCRM(account, data.id);
}
}

Important Considerations

Changes Object Structure

The changes object only includes the fields that actually changed:

  • If only flags changed, changes.labels will be absent
  • If only labels changed, changes.flags will be absent
  • Within each change type, added and deleted arrays are only present if there are items in them

Always check for the existence of these fields before accessing them:

// Safe access pattern
const flagsAdded = data.changes.flags?.added || [];
const flagsDeleted = data.changes.flags?.deleted || [];
const currentFlags = data.changes.flags?.value || [];

IMAP Indexer Mode

For standard IMAP accounts, the messageUpdated event is only triggered when using the "full" IMAP indexer mode (default). In "fast" indexer mode, flag changes are not tracked because the full message list is not maintained.

Recent Flag

The \Recent flag is a session-specific IMAP flag and is not tracked by EmailEngine. Changes to this flag will not trigger messageUpdated events.

Event Deduplication

EmailEngine implements rolling bucket locks to prevent duplicate messageUpdated events for the same message within a short time window. If the same flag change is detected multiple times rapidly, only the first event is sent.

Gmail Labels vs IMAP Flags

For Gmail API accounts, both flags and labels may be present in the same event:

  • Flags represent standard email states (\Seen, \Flagged, \Answered, \Draft)
  • Labels represent Gmail-specific categorization

Some Gmail labels map to IMAP flags:

  • STARRED maps to \Flagged
  • UNREAD absence maps to \Seen

See Also