472 lines
17 KiB
Plaintext
472 lines
17 KiB
Plaintext
---
|
|
title: Webhooks
|
|
description: Send real-time event data from Plunk to your own application using webhooks
|
|
icon: Webhook
|
|
---
|
|
|
|
import {Tab, Tabs} from 'fumadocs-ui/components/tabs';
|
|
|
|
Plunk can send real-time HTTP requests to your application when specific events occur, such as email bounces, spam complaints, or custom events. This is done by creating a [workflow](/concepts/workflows) that uses the **Webhook** step to forward event data to your own endpoint.
|
|
|
|
## How it works
|
|
|
|
Webhooks in Plunk are powered by the workflow system. The basic flow is:
|
|
|
|
1. An event occurs in Plunk (e.g. an email bounces, a contact subscribes, or a custom event is tracked)
|
|
2. A workflow is triggered by that event
|
|
3. The workflow executes a **Webhook** step, sending an HTTP request to your URL with relevant data
|
|
|
|
This means you can receive notifications for any event Plunk tracks, including both system events and your own custom events.
|
|
|
|
## Internal events
|
|
|
|
Plunk automatically tracks a set of internal events that you can use as workflow triggers. These events cannot be manually tracked via the API — they are generated by the system.
|
|
|
|
### Email events
|
|
|
|
| Event | Description |
|
|
| ----------------- | -------------------------------------------------------------------------------------------------------- |
|
|
| `email.sent` | An email was successfully sent |
|
|
| `email.delivery` | An email was delivered to the recipient |
|
|
| `email.open` | A contact opened an email for the first time |
|
|
| `email.click` | A contact clicked a link in an email for the first time |
|
|
| `email.bounce` | An email bounced (hard or soft bounce) |
|
|
| `email.complaint` | A contact marked an email as spam |
|
|
| `email.received` | An email was received at your verified domain (requires [inbound email setup](/guides/receiving-emails)) |
|
|
|
|
### Contact events
|
|
|
|
| Event | Description |
|
|
| ---------------------- | ------------------------------------------------------- |
|
|
| `contact.subscribed` | A contact's subscription status changed to subscribed |
|
|
| `contact.unsubscribed` | A contact's subscription status changed to unsubscribed |
|
|
|
|
### Segment events
|
|
|
|
| Event | Description |
|
|
| ---------------------- | --------------------------- |
|
|
| `segment.<name>.entry` | A contact entered a segment |
|
|
| `segment.<name>.exit` | A contact exited a segment |
|
|
|
|
<Callout title="Segment event names" variant="idea">
|
|
Segment events use a slugified version of the segment name. For example, a segment called "VIP Users" would produce
|
|
the events `segment.vip-users.entry` and `segment.vip-users.exit`.
|
|
</Callout>
|
|
|
|
## Setting up a webhook
|
|
|
|
<div className='fd-steps [&_h3]:fd-step'>
|
|
|
|
### Create the workflow
|
|
|
|
Navigate to the **Workflows** section in the dashboard and create a new workflow. Choose the event you want to listen for as the trigger. For example, to receive notifications when an email bounces, use `email.bounce` as the trigger event.
|
|
|
|
### Add a Webhook step
|
|
|
|
After the trigger, add a **Webhook** step and configure it:
|
|
|
|
- **URL**: The endpoint on your server that will receive the webhook (e.g. `https://api.example.com/webhooks/plunk`)
|
|
- **Method**: The HTTP method to use. Defaults to `POST`, which is recommended for most use cases.
|
|
- **Headers** (optional): Custom headers to include in the request, provided as JSON. This is useful for authentication.
|
|
|
|
```json
|
|
{
|
|
"Authorization": "Bearer your-secret-token"
|
|
}
|
|
```
|
|
|
|
### Enable the workflow
|
|
|
|
Once configured, enable the workflow. It will start sending webhook requests whenever the trigger event occurs.
|
|
|
|
</div>
|
|
|
|
## Webhook payload
|
|
|
|
When using the default payload (no custom body configured), Plunk sends a JSON request with the following structure:
|
|
|
|
```json
|
|
{
|
|
"contact": {
|
|
"email": "user@example.com",
|
|
"subscribed": true,
|
|
"data": {
|
|
"name": "John",
|
|
"plan": "pro"
|
|
}
|
|
},
|
|
"workflow": {
|
|
"id": "wf_abc123",
|
|
"name": "Bounce Notifications"
|
|
},
|
|
"execution": {
|
|
"id": "exec_xyz789",
|
|
"startedAt": "2025-01-15T10:30:00.000Z"
|
|
},
|
|
"event": {
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": "camp_abc123",
|
|
"sourceType": "CAMPAIGN",
|
|
"bounceType": "Permanent",
|
|
"bouncedAt": "2025-01-15T10:30:00.000Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
The `event` field contains the data associated with the event that triggered the workflow. The exact contents depend on the event type.
|
|
|
|
### Event data by type
|
|
|
|
#### Email events
|
|
|
|
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 (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:
|
|
|
|
<Tabs items={['email.sent', 'email.delivery', 'email.open', 'email.click', 'email.bounce', 'email.complaint', 'email.received']}>
|
|
|
|
<Tab value="email.sent">
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": null,
|
|
"sourceType": "TRANSACTIONAL",
|
|
"sentAt": "2025-01-15T10:30:00.000Z"
|
|
}
|
|
```
|
|
|
|
| Field | Description |
|
|
| -------- | ----------------------- |
|
|
| `sentAt` | When the email was sent |
|
|
|
|
</Tab>
|
|
|
|
<Tab value="email.delivery">
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": "camp_abc123",
|
|
"sourceType": "CAMPAIGN",
|
|
"deliveredAt": "2025-01-15T10:30:05.000Z"
|
|
}
|
|
```
|
|
|
|
| Field | Description |
|
|
| ------------- | ---------------------------- |
|
|
| `deliveredAt` | When the email was delivered |
|
|
|
|
</Tab>
|
|
|
|
<Tab value="email.open">
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": null,
|
|
"sourceType": "TRANSACTIONAL",
|
|
"openedAt": "2025-01-15T11:00:00.000Z",
|
|
"opens": 1,
|
|
"isFirstOpen": true
|
|
}
|
|
```
|
|
|
|
| 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 |
|
|
|
|
</Tab>
|
|
|
|
<Tab value="email.click">
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": null,
|
|
"sourceType": "TRANSACTIONAL",
|
|
"link": "https://example.com/pricing",
|
|
"clickedAt": "2025-01-15T11:05:00.000Z",
|
|
"clicks": 1,
|
|
"isFirstClick": true
|
|
}
|
|
```
|
|
|
|
| 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 |
|
|
|
|
</Tab>
|
|
|
|
<Tab value="email.bounce">
|
|
|
|
Permanent bounce:
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": null,
|
|
"sourceType": "TRANSACTIONAL",
|
|
"bounceType": "Permanent",
|
|
"bouncedAt": "2025-01-15T10:31:00.000Z"
|
|
}
|
|
```
|
|
|
|
Transient (soft) bounce:
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": null,
|
|
"sourceType": "TRANSACTIONAL",
|
|
"bounceType": "Transient",
|
|
"transientBounce": true
|
|
}
|
|
```
|
|
|
|
| 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 |
|
|
|
|
<Callout title="Bounce rate impact" variant="warn">
|
|
Only `Permanent` bounces count toward your project's bounce rate and trigger automatic contact unsubscription.
|
|
`Transient` bounces are tracked for visibility only.
|
|
</Callout>
|
|
|
|
</Tab>
|
|
|
|
<Tab value="email.complaint">
|
|
|
|
```json
|
|
{
|
|
"subject": "Welcome to Plunk",
|
|
"from": "hello@example.com",
|
|
"fromName": "Plunk Team",
|
|
"messageId": "ses-message-id",
|
|
"emailId": "ac32f08e-c6b9-45d3-9824-a73dff1e3bbf",
|
|
"templateId": null,
|
|
"campaignId": null,
|
|
"sourceType": "TRANSACTIONAL",
|
|
"complainedAt": "2025-01-15T10:35:00.000Z"
|
|
}
|
|
```
|
|
|
|
| Field | Description |
|
|
| -------------- | ------------------------------------ |
|
|
| `complainedAt` | When the spam complaint was received |
|
|
|
|
</Tab>
|
|
|
|
<Tab value="email.received">
|
|
|
|
This event fires when an email is received at your verified domain. See [Receiving Emails](/guides/receiving-emails) for setup instructions.
|
|
|
|
```json
|
|
{
|
|
"messageId": "ses-message-id",
|
|
"from": "sender@example.com",
|
|
"fromHeader": "Jane Smith <sender@example.com>",
|
|
"to": "support@yourdomain.com",
|
|
"subject": "Re: Your question",
|
|
"timestamp": "2025-01-15T10:30:00.000Z",
|
|
"recipients": ["support@yourdomain.com"],
|
|
"hasContent": true,
|
|
"body": "<html><body>This is the email body content...</body></html>",
|
|
"spamVerdict": "PASS",
|
|
"virusVerdict": "PASS",
|
|
"spfVerdict": "PASS",
|
|
"dkimVerdict": "PASS",
|
|
"dmarcVerdict": "PASS",
|
|
"processingTimeMillis": 142
|
|
}
|
|
```
|
|
|
|
| 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 |
|
|
| `body` | HTML body of the email (or plain text if no HTML 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 |
|
|
|
|
</Tab>
|
|
|
|
</Tabs>
|
|
|
|
#### Contact events
|
|
|
|
`contact.subscribed` and `contact.unsubscribed` carry no event data by default. The `event` field will be an empty object `{}`.
|
|
|
|
The exception is when an unsubscription is triggered automatically by an email bounce or complaint — in that case `event` includes a `reason` field:
|
|
|
|
```json
|
|
{
|
|
"reason": "bounce"
|
|
}
|
|
```
|
|
|
|
| Field | Value |
|
|
| -------- | --------------------------------------------------- |
|
|
| `reason` | `"bounce"` or `"complaint"` (when system-triggered) |
|
|
|
|
#### Segment events
|
|
|
|
Both `segment.<name>.entry` and `segment.<name>.exit` include:
|
|
|
|
```json
|
|
{
|
|
"segmentId": "seg_abc123",
|
|
"segmentName": "VIP Users"
|
|
}
|
|
```
|
|
|
|
| Field | Description |
|
|
| ------------- | ------------------------------- |
|
|
| `segmentId` | The ID of the segment |
|
|
| `segmentName` | The display name of the segment |
|
|
|
|
#### Custom events
|
|
|
|
Custom events tracked via the API include whatever data you passed in the `data` field when calling `track`.
|
|
|
|
#### No event 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
|
|
|
|
Create a workflow triggered by `email.bounce` or `email.complaint` to forward these events to your application. This allows you to keep your own database in sync with Plunk's contact statuses.
|
|
|
|
You can use additional workflow steps before the webhook to add logic:
|
|
|
|
- **Condition**: Only send the webhook for hard bounces by checking the `bounceType` field
|
|
- **Delay**: Add a short delay to batch-process related events
|
|
- **Update Contact**: Mark the contact with metadata before sending the webhook
|
|
|
|
### Syncing unsubscribes
|
|
|
|
Trigger a workflow on `contact.unsubscribed` to notify your application when a contact opts out. This is useful for keeping subscription status synchronized across multiple systems.
|
|
|
|
### Custom event forwarding
|
|
|
|
If you track custom events in Plunk (e.g. `user.signup`, `order.completed`), you can forward those same events to other services via webhooks. This turns Plunk into an event router — track once, distribute to multiple endpoints.
|
|
|
|
## Adding conditions and delays
|
|
|
|
Since webhooks are part of the workflow system, you can combine them with other step types for more advanced setups:
|
|
|
|
- Use a **Condition** step to only fire the webhook when certain criteria are met (e.g. only notify for contacts on a specific plan)
|
|
- Use a **Wait for Event** step to wait for a follow-up event before sending the webhook (e.g. wait to see if a bounced contact re-subscribes)
|
|
- Use a **Delay** step to add a time buffer before the webhook fires
|