diff --git a/apps/api/src/controllers/Webhooks.ts b/apps/api/src/controllers/Webhooks.ts
index 3e80cb0..5550841 100644
--- a/apps/api/src/controllers/Webhooks.ts
+++ b/apps/api/src/controllers/Webhooks.ts
@@ -273,6 +273,7 @@ export class Webhooks {
from: email.from,
fromName: email.fromName,
messageId: email.messageId,
+ emailId: email.id,
templateId: email.templateId,
campaignId: email.campaignId,
sourceType: email.sourceType,
diff --git a/apps/api/src/jobs/email-processor.ts b/apps/api/src/jobs/email-processor.ts
index 77662df..bf78a6a 100644
--- a/apps/api/src/jobs/email-processor.ts
+++ b/apps/api/src/jobs/email-processor.ts
@@ -188,6 +188,7 @@ export async function createEmailWorker() {
from: email.from,
fromName: email.fromName,
messageId: result.messageId,
+ emailId: email.id,
templateId: email.templateId,
campaignId: email.campaignId,
sourceType: email.sourceType,
diff --git a/apps/wiki/content/docs/guides/webhooks.mdx b/apps/wiki/content/docs/guides/webhooks.mdx
index bd503d9..6a24e8c 100644
--- a/apps/wiki/content/docs/guides/webhooks.mdx
+++ b/apps/wiki/content/docs/guides/webhooks.mdx
@@ -108,6 +108,7 @@ When using the default payload (no custom body configured), Plunk sends a JSON r
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": "camp_abc123",
"sourceType": "CAMPAIGN",
@@ -125,15 +126,16 @@ The `event` field contains the data associated with the event that triggered the
Most email events share a common set of base fields:
-| Field | Description |
-| ------------ | ------------------------------------------------------------------------------------------- |
-| `subject` | The email subject line |
-| `from` | The sender email address |
-| `fromName` | The sender display name |
-| `messageId` | The AWS SES message ID |
-| `templateId` | The template ID, if the email was sent using a template (otherwise `null`) |
-| `campaignId` | The campaign ID, if the email was part of a campaign (otherwise `null`) |
-| `sourceType` | How the email was triggered: `TRANSACTIONAL`, `CAMPAIGN`, `WORKFLOW`, or `INBOUND` |
+| Field | Description |
+| ------------ | ------------------------------------------------------------------------------------------------------ |
+| `subject` | The email subject line |
+| `from` | The sender email address |
+| `fromName` | The sender display name |
+| `messageId` | The AWS SES message ID (for correlating with SES events) |
+| `emailId` | The Plunk email record ID (returned from `POST /v1/send`, for correlating webhooks with API responses) |
+| `templateId` | The template ID, if the email was sent using a template (otherwise `null`) |
+| `campaignId` | The campaign ID, if the email was part of a campaign (otherwise `null`) |
+| `sourceType` | How the email was triggered: `TRANSACTIONAL`, `CAMPAIGN`, `WORKFLOW`, or `INBOUND` |
In addition to these base fields, each event includes the following event-specific fields:
@@ -147,6 +149,7 @@ In addition to these base fields, each event includes the following event-specif
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": null,
"sourceType": "TRANSACTIONAL",
@@ -154,9 +157,9 @@ In addition to these base fields, each event includes the following event-specif
}
```
-| Field | Description |
-| -------- | ------------------------ |
-| `sentAt` | When the email was sent |
+| Field | Description |
+| -------- | ----------------------- |
+| `sentAt` | When the email was sent |
@@ -168,6 +171,7 @@ In addition to these base fields, each event includes the following event-specif
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": "camp_abc123",
"sourceType": "CAMPAIGN",
@@ -175,9 +179,9 @@ In addition to these base fields, each event includes the following event-specif
}
```
-| Field | Description |
-| ------------- | ------------------------------- |
-| `deliveredAt` | When the email was delivered |
+| Field | Description |
+| ------------- | ---------------------------- |
+| `deliveredAt` | When the email was delivered |
@@ -189,6 +193,7 @@ In addition to these base fields, each event includes the following event-specif
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": null,
"sourceType": "TRANSACTIONAL",
@@ -198,10 +203,10 @@ In addition to these base fields, each event includes the following event-specif
}
```
-| Field | Description |
-| ------------- | ------------------------------------------------------------ |
-| `openedAt` | When the email was first opened |
-| `opens` | Total number of times this email has been opened |
+| Field | Description |
+| ------------- | ------------------------------------------------------------- |
+| `openedAt` | When the email was first opened |
+| `opens` | Total number of times this email has been opened |
| `isFirstOpen` | `true` if this is the first time the contact opened the email |
@@ -214,6 +219,7 @@ In addition to these base fields, each event includes the following event-specif
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": null,
"sourceType": "TRANSACTIONAL",
@@ -224,11 +230,11 @@ In addition to these base fields, each event includes the following event-specif
}
```
-| Field | Description |
-| -------------- | -------------------------------------------------------------- |
-| `link` | The URL that was clicked |
-| `clickedAt` | When the first click occurred |
-| `clicks` | Total number of times links in this email have been clicked |
+| Field | Description |
+| -------------- | ----------------------------------------------------------------- |
+| `link` | The URL that was clicked |
+| `clickedAt` | When the first click occurred |
+| `clicks` | Total number of times links in this email have been clicked |
| `isFirstClick` | `true` if this is the first click from this contact on this email |
@@ -243,6 +249,7 @@ Permanent bounce:
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": null,
"sourceType": "TRANSACTIONAL",
@@ -259,6 +266,7 @@ Transient (soft) bounce:
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": null,
"sourceType": "TRANSACTIONAL",
@@ -267,14 +275,15 @@ Transient (soft) bounce:
}
```
-| Field | Description |
-| ---------------- | ----------------------------------------------------------------------------------------------- |
-| `bounceType` | `Permanent` (hard bounce) or `Transient` (soft bounce, e.g. mailbox full or out-of-office) |
-| `bouncedAt` | When the bounce occurred (permanent bounces only) |
-| `transientBounce`| `true` for soft bounces — these do not count toward bounce rate and the contact stays subscribed |
+| Field | Description |
+| ----------------- | ------------------------------------------------------------------------------------------------ |
+| `bounceType` | `Permanent` (hard bounce) or `Transient` (soft bounce, e.g. mailbox full or out-of-office) |
+| `bouncedAt` | When the bounce occurred (permanent bounces only) |
+| `transientBounce` | `true` for soft bounces — these do not count toward bounce rate and the contact stays subscribed |
- Only `Permanent` bounces count toward your project's bounce rate and trigger automatic contact unsubscription. `Transient` bounces are tracked for visibility only.
+ Only `Permanent` bounces count toward your project's bounce rate and trigger automatic contact unsubscription.
+ `Transient` bounces are tracked for visibility only.
@@ -287,6 +296,7 @@ Transient (soft) bounce:
"from": "hello@example.com",
"fromName": "Plunk Team",
"messageId": "ses-message-id",
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
"templateId": null,
"campaignId": null,
"sourceType": "TRANSACTIONAL",
@@ -294,9 +304,9 @@ Transient (soft) bounce:
}
```
-| Field | Description |
-| -------------- | ------------------------------------------------ |
-| `complainedAt` | When the spam complaint was received |
+| Field | Description |
+| -------------- | ------------------------------------ |
+| `complainedAt` | When the spam complaint was received |
@@ -323,22 +333,22 @@ This event fires when an email is received at your verified domain. See [Receivi
}
```
-| Field | Description |
-| ---------------------- | ------------------------------------------------------------------- |
-| `messageId` | The AWS SES message ID |
-| `from` | The sender's email address |
-| `fromHeader` | The full `From` header, including display name if present |
-| `to` | The recipient address at your verified domain |
-| `subject` | The email subject line |
-| `timestamp` | When SES received the email |
-| `recipients` | All recipient addresses in the envelope |
-| `hasContent` | Whether the email body content is available |
+| Field | Description |
+| ---------------------- | --------------------------------------------------------------------- |
+| `messageId` | The AWS SES message ID |
+| `from` | The sender's email address |
+| `fromHeader` | The full `From` header, including display name if present |
+| `to` | The recipient address at your verified domain |
+| `subject` | The email subject line |
+| `timestamp` | When SES received the email |
+| `recipients` | All recipient addresses in the envelope |
+| `hasContent` | Whether the email body content is available |
| `spamVerdict` | SES spam check result: `PASS`, `FAIL`, `GRAY`, or `PROCESSING_FAILED` |
-| `virusVerdict` | SES virus check result |
-| `spfVerdict` | SPF authentication result |
-| `dkimVerdict` | DKIM authentication result |
-| `dmarcVerdict` | DMARC authentication result |
-| `processingTimeMillis` | Time SES took to process the inbound email |
+| `virusVerdict` | SES virus check result |
+| `spfVerdict` | SPF authentication result |
+| `dkimVerdict` | DKIM authentication result |
+| `dmarcVerdict` | DMARC authentication result |
+| `processingTimeMillis` | Time SES took to process the inbound email |
@@ -356,8 +366,8 @@ The exception is when an unsubscription is triggered automatically by an email b
}
```
-| Field | Value |
-| -------- | -------------------------------------------------- |
+| Field | Value |
+| -------- | --------------------------------------------------- |
| `reason` | `"bounce"` or `"complaint"` (when system-triggered) |
#### Segment events
@@ -371,9 +381,9 @@ Both `segment..entry` and `segment..exit` include:
}
```
-| Field | Description |
-| ------------- | ----------------------------- |
-| `segmentId` | The ID of the segment |
+| Field | Description |
+| ------------- | ------------------------------- |
+| `segmentId` | The ID of the segment |
| `segmentName` | The display name of the segment |
#### Custom events
@@ -384,6 +394,52 @@ Custom events tracked via the API include whatever data you passed in the `data`
For events that carry no data, the `event` field will be an empty object `{}`.
+## Correlating webhooks with send requests
+
+All email events include an `emailId` field that matches the Plunk email record ID returned when you send an email via `POST /v1/send`. This allows you to directly correlate webhook events with your API requests.
+
+**Example workflow:**
+
+1. Send email via API:
+
+```json
+POST /v1/send
+{
+ "to": "user@example.com",
+ "subject": "Welcome",
+ "body": "Hello!"
+}
+
+Response:
+{
+ "success": true,
+ "data": {
+ "emails": [
+ {
+ "contact": {"id": "cnt_abc", "email": "user@example.com"},
+ "email": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf"
+ }
+ ]
+ }
+}
+```
+
+2. Store the `email` ID (`ac32f08e-c6b9-45d3-9824-a73dff1e3bbf`) in your database
+
+3. When webhook events fire (e.g., `email.open`, `email.bounce`), match them using `event.emailId`:
+
+```json
+{
+ "event": {
+ "emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
+ "messageId": "ses-message-id",
+ "openedAt": "2025-01-15T11:00:00.000Z"
+ }
+}
+```
+
+This eliminates the need to match by contact email + timestamp or to listen for `email.sent` webhooks just to get the SES `messageId`.
+
## Common use cases
### Bounce and complaint monitoring
diff --git a/apps/wiki/openapi.json b/apps/wiki/openapi.json
index 0ede180..0d02380 100644
--- a/apps/wiki/openapi.json
+++ b/apps/wiki/openapi.json
@@ -471,7 +471,7 @@
},
"email": {
"type": "string",
- "description": "Email record ID"
+ "description": "Plunk email record ID. Use this to correlate webhook events (which include this ID as 'emailId' in the event data) with your send requests."
}
}
}
@@ -483,6 +483,21 @@
}
}
}
+ },
+ "example": {
+ "success": true,
+ "data": {
+ "emails": [
+ {
+ "contact": {
+ "id": "cnt_abc123",
+ "email": "user@example.com"
+ },
+ "email": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf"
+ }
+ ],
+ "timestamp": "2025-01-15T10:30:00.000Z"
+ }
}
}
}