Webhooks
Send real-time event data from Plunk to your own application using webhooks
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 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:
- An event occurs in Plunk (e.g. an email bounces, a contact subscribes, or a custom event is tracked)
- A workflow is triggered by that event
- 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) |
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 |
Segment event names
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.
Setting up a webhook
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.
{
"Authorization": "Bearer your-secret-token"
}- Body (optional): Custom request body. When omitted, Plunk sends the default payload shown below. When provided, the value replaces the default payload entirely and is JSON-encoded before being sent.
Use variables in the request (optional)
The url, header values, and body all support {{variable}} interpolation. The available scope is the same as SEND_EMAIL templates, plus a webhook-only event namespace exposing the trigger event payload:
| Variable | Value |
|---|---|
{{id}}, {{email}} | The contact's ID and email. |
{{<key>}} (top-level) | Any key from the contact's data JSON (e.g. {{firstName}}, {{plan}}). |
{{data.<key>}} | The same contact data, addressed via the data namespace. |
{{event.<key>}} | Webhook-only. Fields from the trigger event payload (e.g. {{event.subject}}). |
{{<key>}} (from execution context) | Keys passed in as context when starting a MANUAL execution. |
{{unsubscribeUrl}}, {{subscribeUrl}}, {{manageUrl}} | Per-contact subscription management URLs. |
The HTTP method is not templated — it must be a literal verb (GET, POST, PUT, PATCH, DELETE). The url must include a static scheme (http:// or https://); placeholders are supported inside the URL but cannot replace the scheme.
Example — forward a contact event to your own API, parameterised by contact data:
URL
https://api.example.com/users/{{id}}/eventsHeaders
{
"Authorization": "Bearer your-secret-token"
}Body
{
"email": "{{email}}",
"plan": "{{plan}}",
"referrer": "{{event.referrer}}"
}Enable the workflow
Once configured, enable the workflow. It will start sending webhook requests whenever the trigger event occurs.
Webhook payload
When using the default payload (no custom body configured), Plunk sends a JSON request with the following structure:
{
"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:
Prop
Type
In addition to these base fields, each event includes the following event-specific fields. The base fields above (subject, from, fromName, messageId, emailId, templateId, campaignId, sourceType) are present on every email event in addition to the event-specific fields shown below.
{
"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"
}Prop
Type
{
"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"
}Prop
Type
{
"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
}Prop
Type
{
"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
}Prop
Type
Permanent bounce:
{
"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:
{
"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
}Prop
Type
Bounce rate impact
Only Permanent bounces count toward your project's bounce rate and trigger automatic contact unsubscription.
Transient bounces are tracked for visibility only.
{
"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"
}Prop
Type
This event fires when an email is received at your verified domain. See Receiving Emails for setup instructions.
{
"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
}Prop
Type
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:
{
"reason": "bounce"
}Prop
Type
Segment events
Both segment.<name>.entry and segment.<name>.exit include:
{
"segmentId": "seg_abc123",
"segmentName": "VIP Users"
}Prop
Type
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:
- Send email via API:
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"
}
]
}
}-
Store the
emailID (ac32f08e-c6b9-45d3-9824-a73dff1e3bbf) in your database -
When webhook events fire (e.g.,
email.open,email.bounce), match them usingevent.emailId:
{
"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 provider 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
bounceTypefield - 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.
Receiving webhooks safely
Plunk's webhook step has a few characteristics worth knowing when you build the receiving endpoint:
-
Method: defaults to
POSTwithContent-Type: application/json. You can override the method per step. -
Timeout: each request times out after 10 seconds. Long-running endpoints should accept the request, queue the work, and return
2xxquickly. -
Redirects: up to 5 redirects are followed. Each hop is re-validated against the SSRF rules below.
-
Public URL required: your webhook endpoint must be reachable on the public internet. Webhooks pointed at private or internal addresses (loopback, RFC 1918 ranges, etc.) won't be delivered.
-
Schemes: only
http://andhttps://are accepted. Prefer HTTPS. -
No automatic retries: a non-2xx response or timeout fails the workflow step. Build idempotency into your handler and use workflow logic (a
WAIT_FOR_EVENTstep, a fallback branch) if you need retry semantics. -
Verify authenticity with a shared secret: configure a secret header on the webhook step and check it on your endpoint:
// Webhook step → Headers { "Authorization": "Bearer your-shared-secret" }The secret travels with every request from that step. Rotate it like any other shared secret. Prefer this over IP allowlisting — egress IPs can change.
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