Update wiki
This commit is contained in:
@@ -1,227 +0,0 @@
|
||||
---
|
||||
title: Cart Abandonment Recovery
|
||||
description: Recover abandoned carts with automated emails
|
||||
icon: ShoppingCart
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Send automated recovery emails when users add items to cart but don't complete purchase. Uses a two-email sequence with a discount incentive.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Track `cart_abandoned` and `purchase_completed` events from your app
|
||||
- Create two email templates in dashboard
|
||||
|
||||
## Track cart events
|
||||
|
||||
### Cart abandoned
|
||||
|
||||
When user adds items but leaves without purchasing:
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'cart_abandoned',
|
||||
email: user.email,
|
||||
data: {
|
||||
cartTotal: cart.total,
|
||||
cartUrl: `https://yourstore.com/cart/${cart.id}`,
|
||||
itemCount: cart.items.length,
|
||||
items: cart.items.map(i => ({
|
||||
name: i.product.name,
|
||||
price: i.price,
|
||||
quantity: i.quantity,
|
||||
imageUrl: i.product.imageUrl
|
||||
}))
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Purchase completed
|
||||
|
||||
When user completes checkout:
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'purchase_completed',
|
||||
email: user.email,
|
||||
data: {
|
||||
orderId: order.id,
|
||||
total: order.total
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Build the workflow
|
||||
|
||||
### 1. Create workflow
|
||||
|
||||
Go to **Workflows** → **Create Workflow**
|
||||
|
||||
- **Name:** Cart Abandonment Recovery
|
||||
- **Trigger:** Event - `cart_abandoned`
|
||||
- **Allow re-entry:** Yes (users can abandon multiple times)
|
||||
|
||||
### 2. Add workflow steps
|
||||
|
||||
```
|
||||
[Trigger: cart_abandoned]
|
||||
↓
|
||||
[Delay: 1 hour]
|
||||
↓
|
||||
[Send Email: Cart Reminder]
|
||||
↓
|
||||
[Wait for Event: purchase_completed, timeout: 23 hours]
|
||||
├─ Purchased → [Exit]
|
||||
└─ Timeout → [Send Email: Cart Discount] → [Exit]
|
||||
```
|
||||
|
||||
**Step-by-step:**
|
||||
|
||||
1. **Delay** (1 hour)
|
||||
- Duration: 1
|
||||
- Unit: Hours
|
||||
|
||||
2. **Send Email** (Cart Reminder)
|
||||
- Template: Cart Reminder
|
||||
- Variables: (auto-populated from event data)
|
||||
|
||||
3. **Wait for Event**
|
||||
- Event: `purchase_completed`
|
||||
- Timeout: 23 hours
|
||||
- Connect two paths:
|
||||
- **Event triggered** → Exit
|
||||
- **Timeout** → Continue to discount
|
||||
|
||||
4. **Send Email** (Cart Discount)
|
||||
- Template: Cart Discount
|
||||
- Variables:
|
||||
- All cart data from event
|
||||
- `discountedTotal`: Calculate in template or pass from backend
|
||||
|
||||
5. **Exit**
|
||||
|
||||
### 3. Enable workflow
|
||||
|
||||
Toggle workflow to **ON**
|
||||
|
||||
## Test the workflow
|
||||
|
||||
### Manual test
|
||||
|
||||
1. Go to **Workflows** → Cart Abandonment Recovery → **Executions**
|
||||
2. Click **Create Execution**
|
||||
3. Select test contact
|
||||
4. Provide test data:
|
||||
|
||||
```json
|
||||
{
|
||||
"cartTotal": 149.99,
|
||||
"cartUrl": "https://yourstore.com/cart/test123",
|
||||
"itemCount": 2,
|
||||
"items": [
|
||||
{
|
||||
"name": "Product A",
|
||||
"price": 79.99,
|
||||
"quantity": 1,
|
||||
"imageUrl": "https://cdn.example.com/product-a.jpg"
|
||||
},
|
||||
{
|
||||
"name": "Product B",
|
||||
"price": 69.99,
|
||||
"quantity": 1,
|
||||
"imageUrl": "https://cdn.example.com/product-b.jpg"
|
||||
}
|
||||
],
|
||||
"discountedTotal": 134.99
|
||||
}
|
||||
```
|
||||
|
||||
5. **Start Execution**
|
||||
|
||||
Watch the execution run. For faster testing, temporarily set delay to 1 minute instead of 1 hour.
|
||||
|
||||
### Live test
|
||||
|
||||
1. Trigger `cart_abandoned` event with your email
|
||||
2. Wait 1 hour (or 1 minute if testing with shorter delay)
|
||||
3. Check for first email
|
||||
4. Either:
|
||||
- Complete purchase → workflow ends
|
||||
- Wait 23 hours → receive discount email
|
||||
|
||||
## Improve conversion
|
||||
|
||||
### Add cart item images
|
||||
|
||||
Pass product images in event data and display in email. Visual reminders increase clicks.
|
||||
|
||||
### Personalize timing
|
||||
|
||||
Test different delays:
|
||||
- First email: 30min, 1hr, 2hr
|
||||
- Second email: 12hr, 24hr, 48hr
|
||||
|
||||
Monitor which timing drives best conversion.
|
||||
|
||||
### Increase discount incrementally
|
||||
|
||||
Second email could offer 10%, third email (if you add one) could offer 15%.
|
||||
|
||||
### Segment by cart value
|
||||
|
||||
Create separate workflows for high-value carts (e.g., >$200) with different messaging or larger discounts.
|
||||
|
||||
### Track discount usage
|
||||
|
||||
When user applies discount code, track event:
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'discount_applied',
|
||||
email: user.email,
|
||||
data: {
|
||||
code: 'SAVE10',
|
||||
source: 'cart_abandonment_email'
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
This lets you measure email-driven conversions.
|
||||
|
||||
## Common issues
|
||||
|
||||
**Email sends but cart is already purchased** — Add a condition before sending emails to check if purchase event already occurred.
|
||||
|
||||
**Cart URL expired** — Ensure cart sessions last at least 48 hours, or regenerate cart from saved items.
|
||||
|
||||
**Discount code doesn't work** — Verify code exists in your system before sending email. Auto-generate unique codes per user for better tracking.
|
||||
|
||||
**Too many emails** — Users abandoning multiple carts quickly will enter workflow multiple times. Consider adding a delay condition or rate limiting.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Track more events](/tutorials/event-tracking-integration) for behavior-based workflows
|
||||
- [Build segments](/tutorials/segment-based-targeting) for high-value cart abandoners
|
||||
- [Use conditions](/automation-patterns/conditional-branching) for cart value-based logic
|
||||
@@ -1,475 +0,0 @@
|
||||
---
|
||||
title: Event Tracking Integration
|
||||
description: Track user behavior to trigger workflows
|
||||
icon: Activity
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Track events from your application to trigger workflows and update contact data. Events like `user_signed_up`, `purchase_completed`, `feature_used` can start automated email sequences.
|
||||
|
||||
## Get your public key
|
||||
|
||||
1. Go to [Settings → General]({{DASHBOARD_URL}}/settings)
|
||||
2. Copy your **Public Key** (starts with `pk_`)
|
||||
|
||||
Public keys are safe to use in client-side code.
|
||||
|
||||
## Basic event tracking
|
||||
|
||||
### JavaScript (client-side)
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'button_clicked',
|
||||
email: user.email,
|
||||
data: {
|
||||
buttonName: 'Get Started',
|
||||
page: '/pricing'
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Node.js (server-side)
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'user_signed_up',
|
||||
email: user.email,
|
||||
data: {
|
||||
name: user.name,
|
||||
plan: 'free',
|
||||
signupDate: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
import os
|
||||
|
||||
requests.post('{{API_URL}}/v1/track',
|
||||
headers={
|
||||
'Authorization': f'Bearer {os.environ["PLUNK_PUBLIC_KEY"]}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'event': 'purchase_completed',
|
||||
'email': user.email,
|
||||
'data': {
|
||||
'orderId': order.id,
|
||||
'total': order.total,
|
||||
'items': order.items
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Common events to track
|
||||
|
||||
### User lifecycle
|
||||
|
||||
```javascript
|
||||
// Signup
|
||||
await trackEvent('user_signed_up', user.email, {
|
||||
name: user.name,
|
||||
source: 'google',
|
||||
plan: 'free'
|
||||
});
|
||||
|
||||
// Activation
|
||||
await trackEvent('first_value_achieved', user.email, {
|
||||
action: 'created_first_project',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Upgrade
|
||||
await trackEvent('subscription_upgraded', user.email, {
|
||||
fromPlan: 'free',
|
||||
toPlan: 'premium',
|
||||
mrr: 99
|
||||
});
|
||||
|
||||
// Churn
|
||||
await trackEvent('subscription_cancelled', user.email, {
|
||||
reason: user.cancellationReason,
|
||||
cancelledAt: new Date().toISOString()
|
||||
});
|
||||
```
|
||||
|
||||
### Product engagement
|
||||
|
||||
```javascript
|
||||
// Feature usage
|
||||
await trackEvent('feature_used', user.email, {
|
||||
featureName: 'data_export',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Content interaction
|
||||
await trackEvent('video_watched', user.email, {
|
||||
videoId: 'intro-101',
|
||||
duration: 300,
|
||||
completed: true
|
||||
});
|
||||
|
||||
// Settings changes
|
||||
await trackEvent('settings_updated', user.email, {
|
||||
setting: 'notifications',
|
||||
value: 'enabled'
|
||||
});
|
||||
```
|
||||
|
||||
### E-commerce
|
||||
|
||||
```javascript
|
||||
// Cart
|
||||
await trackEvent('cart_abandoned', user.email, {
|
||||
cartId: cart.id,
|
||||
cartTotal: cart.total,
|
||||
items: cart.items.map(i => i.name)
|
||||
});
|
||||
|
||||
// Purchase
|
||||
await trackEvent('purchase_completed', user.email, {
|
||||
orderId: order.id,
|
||||
total: order.total,
|
||||
paymentMethod: 'credit_card'
|
||||
});
|
||||
|
||||
// Review
|
||||
await trackEvent('review_submitted', user.email, {
|
||||
productId: product.id,
|
||||
rating: 5
|
||||
});
|
||||
```
|
||||
|
||||
## Event naming conventions
|
||||
|
||||
**Use lowercase with underscores:**
|
||||
- ✅ `user_signed_up`
|
||||
- ✅ `purchase_completed`
|
||||
- ❌ `UserSignedUp`
|
||||
- ❌ `purchase-completed`
|
||||
|
||||
**Be specific:**
|
||||
- ✅ `trial_started`
|
||||
- ❌ `event`
|
||||
|
||||
**Use past tense:**
|
||||
- ✅ `email_opened`
|
||||
- ❌ `email_open`
|
||||
|
||||
## Event data best practices
|
||||
|
||||
**Keep data flat when possible:**
|
||||
|
||||
```javascript
|
||||
// Good
|
||||
{
|
||||
name: 'John',
|
||||
plan: 'premium',
|
||||
mrr: 99
|
||||
}
|
||||
|
||||
// Works but harder to use
|
||||
{
|
||||
user: {
|
||||
profile: {
|
||||
name: 'John'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use consistent types:**
|
||||
|
||||
```javascript
|
||||
// Good - number for numeric values
|
||||
{ total: 99.99 }
|
||||
|
||||
// Bad - string for numeric values
|
||||
{ total: "99.99" }
|
||||
```
|
||||
|
||||
**Use ISO dates:**
|
||||
|
||||
```javascript
|
||||
// Good
|
||||
{ signupDate: new Date().toISOString() }
|
||||
|
||||
// Okay but less flexible
|
||||
{ signupDate: '2024-03-15' }
|
||||
```
|
||||
|
||||
## Integrate with React
|
||||
|
||||
### Context provider
|
||||
|
||||
```javascript
|
||||
// EventTrackingContext.js
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const EventTrackingContext = createContext();
|
||||
|
||||
export function EventTrackingProvider({ children }) {
|
||||
const trackEvent = async (event, data = {}) => {
|
||||
const user = getCurrentUser(); // Your auth logic
|
||||
|
||||
if (!user?.email) return;
|
||||
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event,
|
||||
email: user.email,
|
||||
data: {
|
||||
name: user.name,
|
||||
...data
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EventTrackingContext.Provider value={{ trackEvent }}>
|
||||
{children}
|
||||
</EventTrackingContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useEventTracking = () => useContext(EventTrackingContext);
|
||||
```
|
||||
|
||||
### Use in components
|
||||
|
||||
```javascript
|
||||
import { useEventTracking } from './EventTrackingContext';
|
||||
|
||||
function UpgradeButton() {
|
||||
const { trackEvent } = useEventTracking();
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
await upgradePlan('premium');
|
||||
|
||||
await trackEvent('plan_upgraded', {
|
||||
plan: 'premium',
|
||||
source: 'pricing_page'
|
||||
});
|
||||
};
|
||||
|
||||
return <button onClick={handleUpgrade}>Upgrade</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Integrate with Next.js
|
||||
|
||||
### Client component
|
||||
|
||||
```javascript
|
||||
'use client';
|
||||
|
||||
import { trackEvent } from '@/lib/plunk';
|
||||
|
||||
export function SignupForm() {
|
||||
const handleSubmit = async (data) => {
|
||||
const user = await createUser(data);
|
||||
|
||||
// Track event
|
||||
await trackEvent('user_signed_up', user.email, {
|
||||
name: user.name,
|
||||
source: 'homepage'
|
||||
});
|
||||
};
|
||||
|
||||
return <form onSubmit={handleSubmit}>...</form>;
|
||||
}
|
||||
```
|
||||
|
||||
### Server action
|
||||
|
||||
```javascript
|
||||
'use server';
|
||||
|
||||
import { trackEvent } from '@/lib/plunk';
|
||||
|
||||
export async function createProject(formData) {
|
||||
const user = await getCurrentUser();
|
||||
const project = await db.projects.create({
|
||||
name: formData.get('name'),
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
await trackEvent('project_created', user.email, {
|
||||
projectId: project.id,
|
||||
projectName: project.name
|
||||
});
|
||||
|
||||
return project;
|
||||
}
|
||||
```
|
||||
|
||||
## Create a helper function
|
||||
|
||||
```javascript
|
||||
// lib/plunk.js
|
||||
const PLUNK_PUBLIC_KEY = process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY;
|
||||
|
||||
export async function trackEvent(event, email, data = {}) {
|
||||
try {
|
||||
const response = await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event,
|
||||
email,
|
||||
data
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Failed to track event:', error);
|
||||
// Don't throw - tracking shouldn't break your app
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing events
|
||||
|
||||
### View tracked events
|
||||
|
||||
1. Go to **Activity** in Plunk dashboard
|
||||
2. Filter by event type
|
||||
3. View event data payloads
|
||||
|
||||
### Test locally
|
||||
|
||||
```javascript
|
||||
// Track a test event
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer pk_your_key',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'test_event',
|
||||
email: 'your-email@example.com',
|
||||
data: {
|
||||
test: true,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Check Plunk Activity - event should appear within seconds.
|
||||
|
||||
## Connect to workflows
|
||||
|
||||
Once events are tracked, create workflows that trigger on them:
|
||||
|
||||
1. **Workflows** → **Create Workflow**
|
||||
2. Trigger: Event `user_signed_up`
|
||||
3. Build your automation
|
||||
4. Enable workflow
|
||||
|
||||
Now when you track `user_signed_up`, the workflow runs automatically.
|
||||
|
||||
## Performance considerations
|
||||
|
||||
**Don't block user actions:**
|
||||
|
||||
```javascript
|
||||
// Good - fire and forget
|
||||
handleClick() {
|
||||
trackEvent('button_clicked', user.email);
|
||||
// Don't await
|
||||
}
|
||||
|
||||
// Bad - user waits for tracking
|
||||
async handleClick() {
|
||||
await trackEvent('button_clicked', user.email);
|
||||
// User has to wait
|
||||
}
|
||||
```
|
||||
|
||||
**Batch events for bulk operations:**
|
||||
|
||||
```javascript
|
||||
// If importing 1000 users, track events in background
|
||||
async function importUsers(users) {
|
||||
const imported = await db.users.bulkCreate(users);
|
||||
|
||||
// Queue for background processing
|
||||
await queue.add('track-events', {
|
||||
event: 'user_imported',
|
||||
users: imported
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Add retry logic:**
|
||||
|
||||
```javascript
|
||||
async function trackEventWithRetry(event, email, data, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await trackEvent(event, email, data);
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common issues
|
||||
|
||||
**Event tracked but workflow not triggering**
|
||||
- Workflow is enabled
|
||||
- Event name matches exactly (case-sensitive)
|
||||
- Contact exists in Plunk
|
||||
- Contact is subscribed
|
||||
|
||||
**CORS errors in browser**
|
||||
- Use public key (not secret key)
|
||||
- Plunk API allows CORS from all origins
|
||||
|
||||
**Contact not created**
|
||||
- Email must be valid
|
||||
- Contact is created automatically when event is tracked
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Build a workflow](/tutorials/welcome-series-workflow) triggered by events
|
||||
- [Campaigns vs Workflows](/concepts/campaigns-vs-workflows) decision guide
|
||||
- [Stripe integration](/integrations/stripe-billing) for billing events
|
||||
@@ -4,14 +4,12 @@ description: Send a transactional email in 5 minutes
|
||||
icon: Mail
|
||||
---
|
||||
|
||||
## Get your API key
|
||||
## 1. Get your API key
|
||||
|
||||
1. Go to [Settings → General]({{DASHBOARD_URL}}/settings)
|
||||
1. Go to **Settings → General**
|
||||
2. Copy your **Secret Key** (starts with `sk_`)
|
||||
|
||||
**Important:** Use Secret Key server-side only. Never expose it in client code.
|
||||
|
||||
## Send an email
|
||||
## 2. Send the email
|
||||
|
||||
```bash
|
||||
curl -X POST {{API_URL}}/v1/send \
|
||||
@@ -19,55 +17,12 @@ curl -X POST {{API_URL}}/v1/send \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"to": "user@example.com",
|
||||
"subject": "Reset your password",
|
||||
"body": "<p>Click here to reset: <a href=\"https://app.com/reset/abc123\">Reset Password</a></p>",
|
||||
"subscribed": true
|
||||
"subject": "Hello from Plunk",
|
||||
"body": "<p>Your first email!</p>"
|
||||
}'
|
||||
```
|
||||
|
||||
Replace `sk_your_secret_key` and `user@example.com` with your values.
|
||||
|
||||
### With JavaScript
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
body: `<p>Click here: <a href="${resetLink}">Reset Password</a></p>`,
|
||||
subscribed: true
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### With Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
import os
|
||||
|
||||
requests.post('{{API_URL}}/v1/send',
|
||||
headers={
|
||||
'Authorization': f'Bearer {os.environ["PLUNK_SECRET_KEY"]}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'to': user.email,
|
||||
'subject': 'Reset your password',
|
||||
'body': f'<p>Click here: <a href="{reset_link}">Reset Password</a></p>',
|
||||
'subscribed': True
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Use variables
|
||||
|
||||
Make emails dynamic with variables:
|
||||
## 3. With variables
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/send', {
|
||||
@@ -81,97 +36,21 @@ await fetch('{{API_URL}}/v1/send', {
|
||||
subject: 'Reset your password',
|
||||
body: '<p>Hi {{name}}, click here: <a href="{{resetLink}}">Reset</a></p>',
|
||||
name: user.name,
|
||||
resetLink: `https://app.com/reset/${token}`,
|
||||
subscribed: true
|
||||
resetLink: `https://app.com/reset/${token}`
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Variables in the body (`{{name}}`, `{{resetLink}}`) are replaced with the values you provide.
|
||||
## 4. Using a template
|
||||
|
||||
## Use templates
|
||||
|
||||
Instead of passing HTML every time, create reusable templates.
|
||||
|
||||
### Create a template
|
||||
|
||||
1. Go to **Templates** → **Create Template**
|
||||
2. Name: `Password Reset`
|
||||
3. Type: `Transactional`
|
||||
4. Subject: `Reset your password`
|
||||
5. Body:
|
||||
```html
|
||||
<p>Hi {{name}},</p>
|
||||
<p>Click here to reset your password:</p>
|
||||
<p><a href="{{resetLink}}">Reset Password</a></p>
|
||||
<p>This link expires in 1 hour.</p>
|
||||
```
|
||||
|
||||
### Send with template
|
||||
1. Create template in **Templates → Create Template**
|
||||
2. Send using template ID:
|
||||
|
||||
```javascript
|
||||
await fetch('{{API_URL}}/v1/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: user.email,
|
||||
template: 'password-reset', // Use template slug
|
||||
name: user.name,
|
||||
resetLink: `https://app.com/reset/${token}`,
|
||||
subscribed: true
|
||||
})
|
||||
});
|
||||
{
|
||||
to: user.email,
|
||||
template: 'clx123abc456',
|
||||
name: user.name,
|
||||
resetLink: resetUrl
|
||||
}
|
||||
```
|
||||
|
||||
No need to pass `subject` or `body` - they come from the template.
|
||||
|
||||
## Integration example
|
||||
|
||||
```javascript
|
||||
// Express.js password reset endpoint
|
||||
app.post('/forgot-password', async (req, res) => {
|
||||
const { email } = req.body;
|
||||
|
||||
const user = await db.users.findOne({ email });
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
await db.resetTokens.create({ userId: user.id, token, expiresAt: Date.now() + 3600000 });
|
||||
|
||||
// Send email via Plunk
|
||||
await fetch('{{API_URL}}/v1/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.PLUNK_SECRET_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
to: user.email,
|
||||
subject: 'Reset your password',
|
||||
body: `<p>Click here: <a href="https://app.com/reset/${token}">Reset Password</a></p>`,
|
||||
subscribed: true
|
||||
})
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**"Unauthorized" error** — Check your API key. Must be Secret Key (starts with `sk_`).
|
||||
|
||||
**Email not received** — Check spam folder. If using custom domain, verify it in Settings → Domains.
|
||||
|
||||
**Variables not replaced** — Ensure variable names match exactly (case-sensitive).
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Build a workflow](/tutorials/welcome-series-workflow) for automated sequences
|
||||
- [Set up custom domain](/guides/custom-domains) for better deliverability
|
||||
- [Track events](/tutorials/event-tracking-integration) to trigger workflows
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
title: Tutorials
|
||||
description: Step-by-step guides
|
||||
icon: BookOpen
|
||||
---
|
||||
|
||||
## Getting started
|
||||
|
||||
<Cards>
|
||||
<Card icon="Mail" title="Send Your First Email" href="/tutorials/first-transactional-email">
|
||||
Send a transactional email via API
|
||||
</Card>
|
||||
|
||||
<Card icon="Workflow" title="Build a Welcome Series" href="/tutorials/welcome-series-workflow">
|
||||
Create a 3-email automated workflow
|
||||
</Card>
|
||||
|
||||
<Card icon="Send" title="Send a Campaign" href="/tutorials/newsletter-campaign">
|
||||
Broadcast to your audience
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
## Advanced
|
||||
|
||||
<Cards>
|
||||
<Card icon="Users" title="Segment Targeting" href="/tutorials/segment-based-targeting">
|
||||
Filter and target specific audiences
|
||||
</Card>
|
||||
|
||||
<Card icon="Activity" title="Event Tracking" href="/tutorials/event-tracking-integration">
|
||||
Track user behavior to trigger workflows
|
||||
</Card>
|
||||
|
||||
<Card icon="ShoppingCart" title="Cart Abandonment" href="/tutorials/cart-abandonment-automation">
|
||||
Recover abandoned carts automatically
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -1,13 +1,4 @@
|
||||
{
|
||||
"title": "Tutorials",
|
||||
"pages": [
|
||||
"index",
|
||||
"first-transactional-email",
|
||||
"welcome-series-workflow",
|
||||
"newsletter-campaign",
|
||||
"segment-based-targeting",
|
||||
"cart-abandonment-automation",
|
||||
"user-lifecycle-emails",
|
||||
"event-tracking-integration"
|
||||
]
|
||||
"pages": ["first-transactional-email", "welcome-series-workflow", "newsletter-campaign", "segment-based-targeting"]
|
||||
}
|
||||
|
||||
@@ -1,292 +1,39 @@
|
||||
---
|
||||
title: Send a Newsletter Campaign
|
||||
title: Send a Newsletter
|
||||
description: Broadcast an email to your audience
|
||||
icon: Send
|
||||
---
|
||||
|
||||
## Overview
|
||||
## 1. Create a campaign
|
||||
|
||||
Campaigns let you send one-time broadcasts to your contacts from the dashboard. No code required.
|
||||
1. Go to **Campaigns → Create Campaign**
|
||||
2. Name: `March Newsletter`
|
||||
|
||||
## Create a campaign
|
||||
## 2. Write your email
|
||||
|
||||
1. Go to **Campaigns** → **Create Campaign**
|
||||
2. Fill in basic info:
|
||||
- Name: `March Product Update`
|
||||
- Description: `Monthly newsletter for March 2024`
|
||||
- **From:** Your verified domain
|
||||
- **Subject:** `March updates you'll love`
|
||||
- Write your content
|
||||
|
||||
## Write your email
|
||||
## 3. Select audience
|
||||
|
||||
### Email settings
|
||||
Choose one:
|
||||
- **All contacts:** Everyone subscribed
|
||||
- **Segment:** A saved segment
|
||||
- **Filtered:** Custom filters for this campaign
|
||||
|
||||
- **From**: Your email or verified domain
|
||||
- **Subject**: `March product updates you'll love`
|
||||
- **Preview text**: Shows in inbox preview
|
||||
|
||||
### Email content
|
||||
|
||||
Use the visual editor or write HTML:
|
||||
|
||||
```html
|
||||
<h1>What's new in March</h1>
|
||||
|
||||
<p>Hi {{firstName ?? 'there'}},</p>
|
||||
|
||||
<p>We've been busy this month. Here's what's new:</p>
|
||||
|
||||
<h2>🚀 New Feature: Team Collaboration</h2>
|
||||
<p>Invite team members and collaborate in real-time.</p>
|
||||
|
||||
<h2>⚡ Improved Performance</h2>
|
||||
<p>Everything is now 2x faster.</p>
|
||||
|
||||
<h2>📊 New Analytics Dashboard</h2>
|
||||
<p>Better insights into your data.</p>
|
||||
|
||||
<p><a href="https://yourapp.com/changelog">View Full Changelog</a></p>
|
||||
|
||||
<p>Thanks,<br>The Team</p>
|
||||
|
||||
<p><small>
|
||||
You're receiving this because you subscribed to updates.
|
||||
<a href="{{unsubscribeUrl}}">Unsubscribe</a>
|
||||
</small></p>
|
||||
```
|
||||
|
||||
### Use variables
|
||||
|
||||
Available variables:
|
||||
- `{{firstName}}` - Contact's first name
|
||||
- `{{email}}` - Contact's email
|
||||
- `{{id}}` - Contact ID
|
||||
- `{{unsubscribeUrl}}` - Auto-generated unsubscribe link
|
||||
- Any custom contact data fields
|
||||
|
||||
**Fallback values:**
|
||||
```html
|
||||
<p>Hi {{firstName ?? 'there'}},</p>
|
||||
<!-- Shows "Hi John," or "Hi there," if firstName is missing -->
|
||||
```
|
||||
|
||||
## Select your audience
|
||||
|
||||
### All contacts
|
||||
|
||||
Sends to everyone subscribed in your account.
|
||||
|
||||
### Specific segment
|
||||
|
||||
1. Select **Segment** audience type
|
||||
2. Choose a segment (e.g., "Premium Users")
|
||||
3. Campaign sends to all contacts in that segment
|
||||
|
||||
### Filtered audience
|
||||
|
||||
Create custom filters for this campaign only:
|
||||
|
||||
**Example filters:**
|
||||
- `plan` equals `premium`
|
||||
- `lastLoginAt` within `30` days
|
||||
- `country` equals `United States`
|
||||
|
||||
Combine with AND/OR logic.
|
||||
|
||||
## Preview and test
|
||||
|
||||
### Send test email
|
||||
## 4. Test
|
||||
|
||||
1. Click **Send Test**
|
||||
2. Enter your email address
|
||||
3. Check your inbox
|
||||
2. Check your inbox
|
||||
3. Verify links and content
|
||||
|
||||
Verify:
|
||||
- Subject line
|
||||
- Email content
|
||||
- Variables are replaced
|
||||
- Links work
|
||||
- Unsubscribe link present
|
||||
## 5. Send
|
||||
|
||||
## Send or schedule
|
||||
- **Send Now:** Starts immediately
|
||||
- **Schedule:** Pick date and time
|
||||
|
||||
### Send now
|
||||
## 6. Monitor
|
||||
|
||||
1. Click **Send Now**
|
||||
2. Confirm
|
||||
3. Campaign starts sending immediately
|
||||
|
||||
### Schedule for later
|
||||
|
||||
1. Click **Schedule**
|
||||
2. Select date and time
|
||||
3. Confirm
|
||||
|
||||
Campaign will send automatically at scheduled time.
|
||||
|
||||
## Monitor performance
|
||||
|
||||
### Real-time stats
|
||||
|
||||
Go to **Campaigns** → Your campaign to see:
|
||||
|
||||
- **Recipients**: Total contacts targeted
|
||||
- **Sent**: How many emails sent
|
||||
- **Delivered**: Successfully delivered
|
||||
- **Opened**: Unique opens
|
||||
- **Clicked**: Unique clicks
|
||||
- **Bounced**: Failed deliveries
|
||||
|
||||
### Open and click rates
|
||||
|
||||
- **Open rate** = Opened / Delivered × 100%
|
||||
- **Click rate** = Clicked / Delivered × 100%
|
||||
|
||||
**Good benchmarks:**
|
||||
- Open rate: 15-25%
|
||||
- Click rate: 2-5%
|
||||
|
||||
### View in activity
|
||||
|
||||
Go to **Activity** to see:
|
||||
- Individual email opens
|
||||
- Link clicks
|
||||
- Delivery timeline
|
||||
|
||||
## Campaign best practices
|
||||
|
||||
**Subject lines:**
|
||||
- Keep under 50 characters
|
||||
- Avoid spam words (FREE, $$, URGENT)
|
||||
- Personalize: `{{name}}, check out our new feature`
|
||||
- A/B test different subject lines
|
||||
|
||||
**Send timing:**
|
||||
- Tuesday-Thursday perform best
|
||||
- 10am-2pm in recipient's timezone
|
||||
- Avoid Mondays and Fridays
|
||||
- Test what works for your audience
|
||||
|
||||
**Content:**
|
||||
- One clear call-to-action
|
||||
- Mobile-friendly design
|
||||
- Keep under 500 words
|
||||
- Use images sparingly (slow loading)
|
||||
- Always include unsubscribe link
|
||||
|
||||
**Frequency:**
|
||||
- Weekly: Maximum for engaged audiences
|
||||
- Monthly: Safe default
|
||||
- Quarterly: Minimum to stay top-of-mind
|
||||
- Don't email too often - causes unsubscribes
|
||||
|
||||
## Advanced: Segment-based campaigns
|
||||
|
||||
### Example: Product announcement to paying customers
|
||||
|
||||
1. Create segment "Paying Customers":
|
||||
- Filter: `plan` is not `free`
|
||||
- Filter: `subscribed` equals `true`
|
||||
|
||||
2. Create campaign:
|
||||
- Subject: `New premium features just for you`
|
||||
- Audience: Segment "Paying Customers"
|
||||
|
||||
3. Send campaign - only goes to paying customers
|
||||
|
||||
### Example: Re-engagement campaign
|
||||
|
||||
1. Create segment "Inactive Users":
|
||||
- Filter: `lastLoginAt` within `90` days is `false`
|
||||
- Filter: `subscribed` equals `true`
|
||||
|
||||
2. Create campaign:
|
||||
- Subject: `We miss you! Here's what's new`
|
||||
- Content: Highlight recent updates
|
||||
- Special offer: 20% off upgrade
|
||||
|
||||
3. Send or schedule
|
||||
|
||||
## Campaign vs workflow
|
||||
|
||||
**Use Campaign when:**
|
||||
- One-time send (newsletter, announcement)
|
||||
- Manual timing
|
||||
- Same message to everyone
|
||||
- No automation needed
|
||||
|
||||
**Use Workflow when:**
|
||||
- Multi-email sequence needed
|
||||
- Trigger on user action
|
||||
- Delays between emails
|
||||
- Personalized paths (if/then logic)
|
||||
|
||||
See [Campaigns vs Workflows](/concepts/campaigns-vs-workflows) for full comparison.
|
||||
|
||||
## Duplicate and reuse
|
||||
|
||||
### Duplicate a campaign
|
||||
|
||||
1. Go to campaign
|
||||
2. Click **Duplicate**
|
||||
3. Edit content
|
||||
4. Send to same or different audience
|
||||
|
||||
Useful for monthly newsletters - duplicate last month's, update content.
|
||||
|
||||
### Save as template
|
||||
|
||||
If you'll reuse the design:
|
||||
|
||||
1. **Templates** → **Create Template**
|
||||
2. Paste your campaign HTML
|
||||
3. Save
|
||||
|
||||
Now you can create campaigns faster using the template.
|
||||
|
||||
## Cancel a campaign
|
||||
|
||||
### Before sending
|
||||
|
||||
1. Go to campaign (status: Draft or Scheduled)
|
||||
2. Click **Delete**
|
||||
|
||||
### While sending
|
||||
|
||||
1. Go to campaign (status: Sending)
|
||||
2. Click **Cancel**
|
||||
3. Stops queuing new emails (already sent emails can't be recalled)
|
||||
|
||||
### After sending
|
||||
|
||||
Cannot cancel or recall. Sent emails are delivered.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Campaign not sending**
|
||||
- Check campaign status (Draft needs to be sent)
|
||||
- Verify audience has contacts
|
||||
- Ensure contacts are subscribed
|
||||
- Custom domain must be verified
|
||||
|
||||
**Low open rate**
|
||||
- Improve subject line
|
||||
- Check spam folder placement
|
||||
- Verify sender email/domain
|
||||
- Review send time
|
||||
|
||||
**High unsubscribe rate**
|
||||
- Sending too frequently
|
||||
- Content not relevant
|
||||
- Set better expectations at signup
|
||||
- Review targeting
|
||||
|
||||
**Emails going to spam**
|
||||
- Verify custom domain
|
||||
- Avoid spam trigger words
|
||||
- Don't use all caps or excessive punctuation
|
||||
- Warm up new sending domain
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Build a workflow](/tutorials/welcome-series-workflow) for automated sequences
|
||||
- [Create segments](/tutorials/segment-based-targeting) for better targeting
|
||||
- [Set up custom domain](/guides/custom-domains) for better deliverability
|
||||
View stats at **Campaigns → Your campaign**:
|
||||
- Sent, delivered, opened, clicked, bounced
|
||||
|
||||
@@ -1,298 +1,33 @@
|
||||
---
|
||||
title: Segment-Based Targeting
|
||||
description: Target specific audiences with filters
|
||||
title: Target with Segments
|
||||
description: Send to specific audiences
|
||||
icon: Users
|
||||
---
|
||||
|
||||
## Overview
|
||||
## 1. Create a segment
|
||||
|
||||
Segments are dynamic groups of contacts based on filters. Use them to send targeted campaigns or trigger workflows when contacts enter/exit segments.
|
||||
|
||||
## Create a segment
|
||||
|
||||
1. Go to **Segments** → **Create Segment**
|
||||
1. Go to **Segments → Create Segment**
|
||||
2. Name: `Premium Users`
|
||||
3. Description: `Users on premium or enterprise plan`
|
||||
|
||||
## Add filters
|
||||
## 2. Add filters
|
||||
|
||||
### Simple filter
|
||||
|
||||
Filter by a single field:
|
||||
Example: Premium subscribers
|
||||
|
||||
- Field: `plan`
|
||||
- Operator: `equals`
|
||||
- Value: `premium`
|
||||
|
||||
All contacts where `plan` equals `premium` are in this segment.
|
||||
Add more conditions with AND/OR.
|
||||
|
||||
### Multiple filters (AND logic)
|
||||
|
||||
All conditions must be true:
|
||||
|
||||
- `plan` equals `premium`
|
||||
- **AND** `subscribed` equals `true`
|
||||
- **AND** `lastLoginAt` within `30` days
|
||||
|
||||
Only premium users who are subscribed AND logged in recently.
|
||||
|
||||
### Multiple filters (OR logic)
|
||||
|
||||
Any condition can be true:
|
||||
|
||||
- `plan` equals `premium`
|
||||
- **OR** `plan` equals `enterprise`
|
||||
|
||||
Users on either premium or enterprise plan.
|
||||
|
||||
### Complex filters (AND + OR)
|
||||
|
||||
Combine both:
|
||||
|
||||
- `subscribed` equals `true`
|
||||
- **AND** (`plan` equals `premium` **OR** `plan` equals `enterprise`)
|
||||
|
||||
Subscribed users on premium OR enterprise plans.
|
||||
|
||||
## Filter operators
|
||||
|
||||
### Equals
|
||||
|
||||
Exact match:
|
||||
- `plan` equals `premium`
|
||||
- `country` equals `United States`
|
||||
|
||||
### Not equals
|
||||
|
||||
Everything except:
|
||||
- `plan` not equals `free`
|
||||
- `status` not equals `cancelled`
|
||||
|
||||
### Contains
|
||||
|
||||
Partial text match:
|
||||
- `email` contains `@gmail.com`
|
||||
- `companyName` contains `Inc`
|
||||
|
||||
### Greater than / Less than
|
||||
|
||||
Numeric comparisons:
|
||||
- `mrr` greater than `100`
|
||||
- `age` less than `30`
|
||||
- `loginCount` greater than `10`
|
||||
|
||||
### Exists / Does not exist
|
||||
|
||||
Field has any value:
|
||||
- `phoneNumber` exists
|
||||
- `referralCode` does not exist
|
||||
|
||||
### Within
|
||||
|
||||
Time-based (requires ISO date):
|
||||
- `signupDate` within `7` days
|
||||
- `lastLoginAt` within `30` days
|
||||
- `trialExpiresAt` within `3` days
|
||||
|
||||
## Example segments
|
||||
|
||||
### Active users
|
||||
|
||||
Users who logged in recently:
|
||||
|
||||
- `lastLoginAt` within `7` days
|
||||
- **AND** `subscribed` equals `true`
|
||||
|
||||
### High-value customers
|
||||
|
||||
Users spending over $100/month:
|
||||
|
||||
- `mrr` greater than `100`
|
||||
- **AND** `plan` is not `free`
|
||||
|
||||
### Trial expiring soon
|
||||
|
||||
Users whose trial ends in 3 days:
|
||||
|
||||
- `trialExpiresAt` within `3` days
|
||||
- **AND** `plan` equals `trial`
|
||||
|
||||
### Inactive users
|
||||
|
||||
Haven't logged in for 30+ days:
|
||||
|
||||
- `lastLoginAt` within `30` days is `false`
|
||||
- **AND** `subscribed` equals `true`
|
||||
- **AND** `status` equals `active`
|
||||
|
||||
**Note:** To check "NOT within", set the within filter and toggle the NOT operator.
|
||||
|
||||
### Power users
|
||||
|
||||
High engagement score:
|
||||
|
||||
- `loginCount` greater than `50`
|
||||
- **AND** `featureUsageCount` greater than `100`
|
||||
|
||||
### Geographic targeting
|
||||
|
||||
Specific country or region:
|
||||
|
||||
- `country` equals `United States`
|
||||
- **AND** `state` equals `California`
|
||||
|
||||
### Feature adopters
|
||||
|
||||
Used a specific feature:
|
||||
|
||||
- Custom event filter: `feature_used` triggered
|
||||
- **AND** event data: `featureName` equals `advanced_analytics`
|
||||
|
||||
## Use segments in campaigns
|
||||
|
||||
### Target a segment
|
||||
## 3. Use in campaigns
|
||||
|
||||
1. Create campaign
|
||||
2. Audience: Select **Segment**
|
||||
3. Choose `Premium Users`
|
||||
|
||||
## 4. Use in workflows
|
||||
|
||||
1. Create workflow
|
||||
2. Trigger: **Segment entry**
|
||||
3. Choose your segment
|
||||
4. Campaign sends only to contacts in that segment
|
||||
|
||||
### Preview count
|
||||
|
||||
Before sending, see how many contacts match:
|
||||
- Shows estimated recipient count
|
||||
- Updates in real-time as you adjust filters
|
||||
|
||||
## Use segments in workflows
|
||||
|
||||
### Trigger on segment entry
|
||||
|
||||
Create workflow that runs when contacts enter a segment:
|
||||
|
||||
1. **Workflows** → **Create Workflow**
|
||||
2. Trigger: Segment `trial_expiring_soon`
|
||||
3. Trigger condition: Contact **enters** segment
|
||||
4. Add email: Trial expiration reminder
|
||||
|
||||
When a contact enters "trial_expiring_soon" segment, workflow triggers.
|
||||
|
||||
### Trigger on segment exit
|
||||
|
||||
Run workflow when contacts leave a segment:
|
||||
|
||||
1. Trigger: Segment `active_users`
|
||||
2. Trigger condition: Contact **exits** segment
|
||||
3. Add workflow: Re-engagement sequence
|
||||
|
||||
When user becomes inactive (exits "active_users"), re-engagement starts.
|
||||
|
||||
## Track membership changes
|
||||
|
||||
Enable to trigger events when contacts enter/exit:
|
||||
|
||||
1. Edit segment
|
||||
2. Toggle **Track Membership**
|
||||
3. Save
|
||||
|
||||
Now when contacts move in/out of the segment:
|
||||
- Event `segment_entry_[segment_id]` is tracked
|
||||
- Event `segment_exit_[segment_id]` is tracked
|
||||
- Can use these events in other workflows
|
||||
|
||||
**Performance note:** Only enable for segments you'll use for triggers. Adds processing overhead.
|
||||
|
||||
## Segment best practices
|
||||
|
||||
**Keep it simple**
|
||||
- 3-5 filters per segment max
|
||||
- Avoid deeply nested conditions
|
||||
- Test with expected contacts
|
||||
|
||||
**Use consistent data**
|
||||
- Store dates as ISO strings
|
||||
- Use numbers for numeric values
|
||||
- Consistent field naming
|
||||
|
||||
**Name clearly**
|
||||
- ✅ `Premium Users - Active`
|
||||
- ❌ `Segment 1`
|
||||
|
||||
**Monitor size**
|
||||
- Check segment count regularly
|
||||
- Too large = slow processing
|
||||
- Too small = not enough data
|
||||
|
||||
## Update segments
|
||||
|
||||
Segments update automatically:
|
||||
- When contact data changes
|
||||
- When contacts are added/removed
|
||||
- Typically updates within minutes
|
||||
|
||||
Force refresh:
|
||||
1. Go to segment
|
||||
2. Click **Refresh Count**
|
||||
|
||||
## Advanced: Multi-level targeting
|
||||
|
||||
### Premium users in specific region
|
||||
|
||||
- `plan` equals `premium`
|
||||
- **AND** `country` equals `United States`
|
||||
- **AND** `lastLoginAt` within `7` days
|
||||
|
||||
### Churn risk scoring
|
||||
|
||||
- `lastLoginAt` within `30` days is `false`
|
||||
- **AND** `supportTickets` greater than `3`
|
||||
- **AND** `npsScore` less than `7`
|
||||
|
||||
### Upsell targeting
|
||||
|
||||
- `plan` equals `free`
|
||||
- **AND** `featureUsageCount` greater than `50`
|
||||
- **AND** `teamSize` greater than `5`
|
||||
|
||||
Users on free plan who are power users with teams (good upsell candidates).
|
||||
|
||||
## Performance at scale
|
||||
|
||||
Segments work efficiently with millions of contacts:
|
||||
|
||||
- Indexed queries for fast filtering
|
||||
- Cursor-based pagination
|
||||
- Background count computation
|
||||
|
||||
**Tips for large segments:**
|
||||
- Use specific filters (avoid `contains` on large text fields)
|
||||
- Store commonly queried data as top-level fields
|
||||
- Use numeric comparisons when possible (faster than text)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Segment count is 0 but should have contacts**
|
||||
- Check filter logic (AND vs OR)
|
||||
- Verify field names match exactly (case-sensitive)
|
||||
- Ensure contacts have the required fields
|
||||
- Try simpler filters to debug
|
||||
|
||||
**Segment not updating**
|
||||
- Click **Refresh Count** to force update
|
||||
- Check that contact data was actually updated
|
||||
- Segments typically update within 5 minutes
|
||||
|
||||
**Workflow not triggering on segment entry**
|
||||
- Workflow is enabled
|
||||
- Track Membership is enabled on segment
|
||||
- Trigger is set to segment entry (not exit)
|
||||
|
||||
**Too many contacts in segment**
|
||||
- Filters too broad
|
||||
- Add more specific conditions
|
||||
- Use AND logic instead of OR
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Send a campaign](/tutorials/newsletter-campaign) to a segment
|
||||
- [Build a workflow](/tutorials/welcome-series-workflow) triggered by segment changes
|
||||
- [Track events](/tutorials/event-tracking-integration) to update contact data
|
||||
4. Enable **Track membership changes** on the segment
|
||||
|
||||
@@ -6,31 +6,26 @@ icon: Workflow
|
||||
|
||||
## What you'll build
|
||||
|
||||
An automated workflow that sends 3 emails when users sign up:
|
||||
- Day 0: Welcome email (immediate)
|
||||
- Day 1: Feature tour (24 hours later)
|
||||
- Day 3: Help offer (48 hours after that)
|
||||
- Day 1: Feature tour
|
||||
- Day 3: Help offer
|
||||
|
||||
## Create the workflow
|
||||
## 1. Create templates
|
||||
|
||||
1. **Workflows** → **Create Workflow**
|
||||
Create 3 templates in **Templates → Create Template**:
|
||||
- `Welcome` (use `{{name}}` for personalization)
|
||||
- `Feature Tour`
|
||||
- `Help Offer`
|
||||
|
||||
## 2. Create the workflow
|
||||
|
||||
1. Go to **Workflows → Create Workflow**
|
||||
2. Name: `Welcome Series`
|
||||
3. Trigger Event: `user_signed_up`
|
||||
4. Allow Re-entry: `No` (users get this once)
|
||||
5. Click **Create**
|
||||
3. Trigger: Event `user_signed_up`
|
||||
4. Allow Re-entry: No
|
||||
|
||||
## Build the flow
|
||||
## 3. Build the flow
|
||||
|
||||
In the visual editor:
|
||||
|
||||
1. **Add Send Email** step → Select your welcome template
|
||||
2. **Add Delay** step → 1 day
|
||||
3. **Add Send Email** step → Select your feature tour template
|
||||
4. **Add Delay** step → 2 days
|
||||
5. **Add Send Email** step → Select your help offer template
|
||||
6. **Add Exit** step
|
||||
|
||||
Your flow:
|
||||
```
|
||||
[Trigger: user_signed_up]
|
||||
↓
|
||||
@@ -47,110 +42,27 @@ Your flow:
|
||||
[Exit]
|
||||
```
|
||||
|
||||
7. **Enable** the workflow (toggle switch)
|
||||
## 4. Enable the workflow
|
||||
|
||||
## Track the signup event
|
||||
Toggle the workflow ON.
|
||||
|
||||
Add event tracking to your app when users sign up.
|
||||
## 5. Track signups
|
||||
|
||||
### JavaScript
|
||||
Add to your app:
|
||||
|
||||
```javascript
|
||||
// After successful signup
|
||||
await fetch('{{API_URL}}/v1/track', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_PLUNK_PUBLIC_KEY}`,
|
||||
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
event: 'user_signed_up',
|
||||
email: user.email,
|
||||
data: {
|
||||
name: user.name,
|
||||
dashboardUrl: 'https://app.yourapp.com/dashboard',
|
||||
docsUrl: 'https://docs.yourapp.com'
|
||||
}
|
||||
data: { name: user.name }
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
requests.post('{{API_URL}}/v1/track',
|
||||
headers={
|
||||
'Authorization': f'Bearer {os.environ["PLUNK_PUBLIC_KEY"]}',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
json={
|
||||
'event': 'user_signed_up',
|
||||
'email': user.email,
|
||||
'data': {
|
||||
'name': user.name,
|
||||
'dashboardUrl': 'https://app.yourapp.com/dashboard',
|
||||
'docsUrl': 'https://docs.yourapp.com'
|
||||
}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Important:** Use your **Public Key** (starts with `pk_`) for event tracking.
|
||||
|
||||
## Test the workflow
|
||||
|
||||
### Manual test
|
||||
|
||||
1. Go to **Workflows** → Your workflow → **Executions** tab
|
||||
2. Click **Create Execution**
|
||||
3. Select a test contact
|
||||
4. Add test data:
|
||||
```json
|
||||
{
|
||||
"name": "Test User",
|
||||
"dashboardUrl": "https://app.yourapp.com",
|
||||
"docsUrl": "https://docs.yourapp.com"
|
||||
}
|
||||
```
|
||||
5. **Start Execution**
|
||||
|
||||
Check your email - you should receive the welcome email immediately. The workflow will pause at the delay steps.
|
||||
|
||||
### Faster testing
|
||||
|
||||
For testing, temporarily change delays to 5 minutes instead of days. Test the flow, then change back.
|
||||
|
||||
## Monitor performance
|
||||
|
||||
1. **Workflows** → Your workflow
|
||||
2. Check **Executions** to see who's in the workflow
|
||||
3. Go to **Activity** to see email opens/clicks
|
||||
4. Track open rates for each email
|
||||
|
||||
Typical good rates:
|
||||
- Email 1: 60-80% open rate
|
||||
- Email 2: 40-60% open rate
|
||||
- Email 3: 30-50% open rate
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Workflow not triggering** — Check:
|
||||
- Workflow is enabled (toggle ON)
|
||||
- Event name matches exactly: `user_signed_up`
|
||||
- Using Public Key for tracking
|
||||
- Contact exists and is subscribed
|
||||
|
||||
**Email not sending** — Check:
|
||||
- Template exists
|
||||
- Contact is subscribed
|
||||
- Variables in event data match template variables
|
||||
|
||||
**Duplicate emails** — Ensure `Allow Re-entry` is `No`.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Add conditional logic](/automation-patterns/conditional-branching) for different user types
|
||||
- [Track more events](/tutorials/event-tracking-integration) to trigger workflows
|
||||
- [Build cart abandonment](/tutorials/cart-abandonment-automation) workflow
|
||||
Use your **Public Key** (starts with `pk_`).
|
||||
|
||||
Reference in New Issue
Block a user