Update wiki

This commit is contained in:
Dries Augustyns
2025-12-07 10:25:18 +01:00
parent b2ecf60a13
commit 0003e44db8
23 changed files with 3336 additions and 5 deletions
@@ -0,0 +1,129 @@
---
title: Campaigns vs Workflows
description: Choose the right tool for sending emails
icon: GitCompare
---
## When to use each
**Transactional API** (`/v1/send`) — Immediate one-off emails from your code
- Password resets, confirmations, receipts
- Triggered directly by user actions
- Instant delivery
**Campaigns** — One-time broadcasts to many contacts
- Newsletters, announcements, promotions
- Created in dashboard, send now or schedule
- No code required
**Workflows** — Automated multi-email sequences
- Onboarding series, abandoned cart, trial reminders
- Triggered by events, segment changes, or schedules
- Delays, conditions, multiple emails
## Transactional API
Send emails directly from your application code.
```javascript
await fetch('{{API_URL}}/v1/send', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_SECRET_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: user.email,
subject: 'Reset your password',
body: `Click here: ${resetLink}`
})
});
```
**Use for:**
- Emails that must send immediately
- One-to-one triggered emails
- Context-specific data (reset tokens, order IDs)
## Campaigns
Create and send broadcasts in the dashboard.
1. Create campaign
2. Write email content
3. Select audience (all contacts, segment, or filter)
4. Send now or schedule
**Use for:**
- One-time sends to many people
- Scheduled announcements
- Manual email sends
**Can't do:**
- Automation or triggers
- Multi-step sequences
- Delays between emails
## Workflows
Build automated sequences with the visual workflow builder.
**Example workflow:**
```
[Trigger: user_signed_up]
[Send Email: Welcome]
[Delay: 24 hours]
[Send Email: Feature tour]
[Delay: 48 hours]
[Send Email: Help offer]
```
**Use for:**
- Multi-email sequences
- Time-delayed follow-ups
- Event-triggered automation
- Conditional logic (if/then)
**Setup required:**
- Track events from your app via `/v1/track`
- Create workflow in dashboard
- Enable workflow
## Decision matrix
| Need | Use |
|------|-----|
| Send password reset now | Transactional API |
| Send monthly newsletter | Campaign |
| Send welcome series over 3 days | Workflow |
| Send order confirmation | Transactional API |
| Send announcement to all users | Campaign |
| Send trial reminder 3 days before expiration | Workflow |
| Send receipt after payment | Transactional API |
| Send seasonal promotion | Campaign |
| Send abandoned cart recovery (1hr + 24hr) | Workflow |
## Using them together
Most apps use all three:
**SaaS example:**
- **Transactional**: Password resets, email verification
- **Campaigns**: Monthly product updates
- **Workflows**: Trial onboarding, churn prevention
**E-commerce example:**
- **Transactional**: Order confirmations, shipping updates
- **Campaigns**: Weekly deals newsletter
- **Workflows**: Abandoned cart, review requests
## Next steps
- [Send a transactional email](/tutorials/first-transactional-email)
- [Create your first workflow](/tutorials/welcome-series-workflow)
- [Build a campaign](/tutorials/newsletter-campaign)
@@ -0,0 +1,214 @@
---
title: Contacts and Data
description: Store and manage your audience
icon: Users
---
## What are contacts
Contacts are people in your email list. Each contact has:
- **Email** — Unique identifier
- **Subscription status** — Subscribed or unsubscribed
- **Custom data** — Any fields you need
## Data structure
```json
{
"id": "contact_abc123",
"email": "[email protected]",
"subscribed": true,
"data": {
"firstName": "Sarah",
"plan": "pro",
"mrr": 99
}
}
```
The `data` field stores custom information as key-value pairs.
## Data types
**Strings:**
```json
{ "firstName": "Sarah", "company": "Acme Inc" }
```
**Numbers:**
```json
{ "mrr": 99, "loginCount": 15 }
```
Store as numbers for `greaterThan`/`lessThan` comparisons.
**Booleans:**
```json
{ "verified": true, "newsletter": false }
```
**Dates:**
```json
{ "signupDate": "2024-03-15T10:30:00Z" }
```
Use ISO 8601 format.
**Arrays:**
```json
{ "tags": ["vip", "enterprise"] }
```
**Objects:**
```json
{
"address": {
"city": "San Francisco",
"country": "US"
}
}
```
Access nested fields: `address.country`
## Creating contacts
### Via API
```bash
curl -X POST {{API_URL}}/contacts \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"subscribed": true,
"data": {
"firstName": "Sarah",
"plan": "pro"
}
}'
```
### Automatic upsert
If email exists, updates instead of creating duplicate.
```javascript
// First call
POST /contacts { email: "[email protected]", data: { plan: "free" } }
// Second call - updates same contact
POST /contacts { email: "[email protected]", data: { mrr: 99 } }
// Result: { plan: "free", mrr: 99 }
```
Data merges automatically.
### Via event tracking
```bash
curl -X POST {{API_URL}}/v1/track \
-H "Authorization: Bearer pk_your_public_key" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"event": "signed_up",
"data": {
"plan": "pro",
"source": "landing"
}
}'
```
Creates contact if doesn't exist, updates if does.
## Updating contacts
```bash
curl -X PATCH {{API_URL}}/contacts/contact_id \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"data": {
"plan": "premium",
"mrr": 199
}
}'
```
New fields added, existing fields overwritten, unmentioned fields preserved.
### Remove fields
Set to `null`:
```json
{ "data": { "temporaryToken": null } }
```
## Subscription status
**Subscribed (true):**
- Receives marketing emails
- Receives transactional emails
**Unsubscribed (false):**
- Does NOT receive marketing emails
- Still receives transactional emails
Template type controls this behavior. See [Template Types](/concepts/templates-types).
## Using contact data
### In templates
```html
<h1>Hi {{firstName}}!</h1>
<p>Your {{plan}} plan renews on {{renewalDate}}.</p>
```
### In segments
Filter by data fields:
- `plan equals "premium"`
- `mrr greaterThan 100`
- `loginCount lessThan 5`
### In workflows
```
[Condition: plan equals "enterprise"]
├─ True → [Send: Enterprise content]
└─ False → [Send: Standard content]
```
## Best practices
**Consistent naming** — Use camelCase or snake_case, not both.
**Correct types** — Use `99` not `"99"` for numbers.
**ISO dates** — `"2024-03-15T10:30:00Z"` for date fields.
**Sync critical fields only** — Don't mirror entire database. Only fields used in emails, segments, or workflows.
**Update in real-time** — When user data changes, update contact immediately.
**Respect unsubscribes** — Never re-subscribe automatically.
## Deleting contacts
```bash
curl -X DELETE {{API_URL}}/contacts/contact_id \
-H "Authorization: Bearer sk_your_secret_key"
```
Permanent deletion. Consider unsubscribing instead to preserve history.
## Next steps
- [Create segments](/concepts/segments-and-filters)
- [Track events](/concepts/events-and-triggers)
- [Use templates](/concepts/templates-types)
@@ -0,0 +1,180 @@
---
title: Events and Triggers
description: Track behavior and trigger workflows
icon: Activity
---
## What are events
Events track user actions from your application. Use them to update contact data, trigger workflows, and build segments.
## Tracking events
### Basic event
```javascript
await fetch('{{API_URL}}/v1/track', {
method: 'POST',
headers: {
'Authorization': `Bearer ${PLUNK_PUBLIC_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
event: 'signed_up',
email: user.email
})
});
```
Creates or updates contact. Event is recorded.
### Event with data
```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: {
plan: 'premium',
mrr: 99
}
})
});
```
Updates contact data fields. Data persists on contact record.
## Public vs Secret keys
### Public Key (pk_*)
- Safe for client-side code
- Only works with `/v1/track`
- Use in browser/mobile apps
### Secret Key (sk_*)
- Server-side only
- Works with all endpoints
- Full API access
## Event naming
Use clear, past-tense verbs with underscores:
✅ `signed_up`, `purchase_completed`, `feature_activated`
❌ `signup`, `buy`, `feature`
## Workflow triggers
### Event trigger
Workflow starts when event is tracked.
```
Trigger: event = "signed_up"
```
Track event:
```javascript
track('signed_up', '[email protected]');
```
Workflow starts for that contact.
### Wait for event
Workflow pauses until event occurs.
```
[Wait for Event: purchase_completed, timeout: 7 days]
├─ Event occurred → [Send: Thank you]
└─ Timeout → [Send: Discount offer]
```
## Common patterns
### User lifecycle
```javascript
// Signup
track('signed_up', email, { source: 'landing' });
// First login
track('first_login', email, { lastLoginAt: new Date().toISOString() });
// Subscription
track('subscription_created', email, { plan: 'premium', mrr: 99 });
```
### E-commerce
```javascript
// Cart abandonment
track('cart_abandoned', email, {
cartTotal: 149.99,
cartUrl: `https://store.com/cart/${cartId}`
});
// Purchase
track('purchase_completed', email, {
orderId: order.id,
orderTotal: order.total
});
```
## Event-based segments
Track events to update contact data, then segment on that data.
1. Track login:
```javascript
track('login', email, {
lastLoginAt: new Date().toISOString()
});
```
2. Create segment:
```
Field: lastLoginAt
Operator: greaterThan
Value: {{7_days_ago}}
```
Contacts auto-join segment when they log in.
## Testing events
**Dashboard:** Contacts → Search email → Events tab
Shows all events for that contact.
**Workflows:** Check executions after tracking event to verify workflow triggered.
## Rate limits
- Public key: 100 requests/minute
- Secret key: 1000 requests/minute
## Common issues
**Workflow not triggering:**
- Workflow is enabled
- Event name matches exactly (case-sensitive)
- Contact email is correct
**Contact data not updating:**
- Field names are case-sensitive
- Values are correct type (number vs string)
## Next steps
- [Build event-triggered workflows](/tutorials/welcome-series-workflow)
- [Create event-based segments](/concepts/segments-and-filters)
- [Track events from your app](/tutorials/event-tracking-integration)
+49
View File
@@ -0,0 +1,49 @@
---
title: Core Concepts
description: Understand how Plunk works
icon: Lightbulb
---
## Email methods
<Cards>
<Card icon="GitCompare" title="Campaigns vs Workflows" href="/concepts/campaigns-vs-workflows">
When to use each email method
</Card>
</Cards>
## Data & contacts
<Cards>
<Card icon="Users" title="Contacts and Data" href="/concepts/contacts-and-data">
Store and manage your audience
</Card>
<Card icon="Filter" title="Segments" href="/concepts/segments-and-filters">
Create dynamic audience groups
</Card>
</Cards>
## Sending emails
<Cards>
<Card icon="Mail" title="Template Types" href="/concepts/templates-types">
Marketing vs Transactional templates
</Card>
<Card icon="Activity" title="Events and Triggers" href="/concepts/events-and-triggers">
Track behavior and trigger workflows
</Card>
</Cards>
## Performance & delivery
<Cards>
<Card icon="Send" title="Email Deliverability" href="/concepts/email-deliverability">
Reach the inbox
</Card>
<Card icon="Zap" title="Scale and Performance" href="/concepts/scale-and-performance">
Optimize for millions of contacts
</Card>
</Cards>
+13
View File
@@ -0,0 +1,13 @@
{
"title": "Core Concepts",
"pages": [
"index",
"contacts-and-data",
"templates-types",
"campaigns-vs-workflows",
"segments-and-filters",
"events-and-triggers",
"email-deliverability",
"scale-and-performance"
]
}
@@ -0,0 +1,150 @@
---
title: Segments and Filters
description: Create dynamic audience groups
icon: Filter
---
## What are segments
Segments are dynamic groups of contacts based on data filters. They update automatically when contact data changes.
Use segments to:
- Target specific audiences in campaigns
- Trigger workflows when contacts enter/exit
- Filter contacts in dashboard
## Creating segments
### In dashboard
**Contacts** → **Segments** → **Create Segment**
1. Name your segment
2. Add filters
3. Save
### Via API
```bash
curl -X POST {{API_URL}}/segments \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Users",
"filters": [
{
"field": "plan",
"operator": "equals",
"value": "premium"
}
]
}'
```
## Filter operators
| Operator | Use | Example |
|----------|-----|---------|
| `equals` | Exact match | `plan equals "pro"` |
| `notEquals` | Not matching | `plan notEquals "free"` |
| `contains` | Substring (case-insensitive) | `email contains "@company.com"` |
| `greaterThan` | Number comparison | `mrr greaterThan 100` |
| `lessThan` | Number comparison | `loginCount lessThan 5` |
| `greaterThanOrEqual` | Inclusive comparison | `age greaterThanOrEqual 18` |
| `lessThanOrEqual` | Inclusive comparison | `daysInactive lessThanOrEqual 30` |
| `exists` | Field has value | `company exists` |
| `notExists` | Field missing/null | `lastName notExists` |
| `startsWith` | String prefix | `coupon startsWith "SAVE"` |
| `endsWith` | String suffix | `email endsWith ".edu"` |
## Multiple filters (AND logic)
All filters must match.
```json
{
"name": "Active Premium Users",
"filters": [
{ "field": "plan", "operator": "equals", "value": "premium" },
{ "field": "loginCount", "operator": "greaterThan", "value": 5 }
]
}
```
Contact matches only if: `plan = "premium"` AND `loginCount > 5`
## Dynamic updates
Segments update automatically.
**Example:**
1. Segment: `plan equals "premium"`
2. User upgrades: `plan` changes from "free" to "premium"
3. Contact auto-added to segment
No manual refresh needed.
## Workflow triggers
Trigger workflows when contacts enter/exit segments.
### Enable tracking
```bash
curl -X PATCH {{API_URL}}/segments/segment_id \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"trackMembership": true}'
```
Only enable for segments used as triggers.
### Entry trigger
```
[Trigger: contact enters "Premium Users"]
[Send: Welcome to premium]
```
### Exit trigger
```
[Trigger: contact exits "Trial Users"]
[Send: Trial ended offer]
```
## Nested fields
Access nested data with dot notation.
```json
{
"field": "preferences.newsletter",
"operator": "equals",
"value": true
}
```
## Best practices
**Store correct types** — Use `99` not `"99"` for numbers. Use `true` not `"true"` for booleans.
**Track membership sparingly** — Only enable on segments used as workflow triggers.
**Name descriptively** — "High-Value Customers (MRR >$200)" not "Segment 3"
## Common issues
**Numeric filters not working** — Ensure field is stored as number, not string.
**Case-sensitive matching** — `equals "Pro"` doesn't match `plan = "pro"`. Use `contains` for case-insensitive.
**Missing field** — If field doesn't exist on contact, filter won't match.
## Next steps
- [Use segments in campaigns](/guides/campaigns)
- [Trigger workflows on segment entry](/guides/workflows)
- [Store contact data](/concepts/contacts-and-data)
@@ -0,0 +1,112 @@
---
title: Template Types
description: Marketing vs Transactional templates
icon: Mail
---
## Two template types
| Template Type | Sends to Unsubscribed? | Use For |
|--------------|------------------------|---------|
| **Marketing** | No | Newsletters, promotions, announcements |
| **Transactional** | Yes | Receipts, confirmations, password resets |
## Marketing templates
Only sends to subscribed contacts.
**Use for:** Newsletters, product updates, promotional emails.
**Behavior:**
```javascript
// Contact is unsubscribed
POST /v1/send { to: "[email protected]", template: "newsletter" }
// → Email NOT sent
```
## Transactional templates
Sends regardless of subscription status.
**Use for:** Order confirmations, password resets, account alerts.
**Behavior:**
```javascript
// Contact is unsubscribed
POST /v1/send { to: "[email protected]", template: "receipt" }
// → Email sent
```
## Setting template type
### In dashboard
**Templates** → Create/Edit → **Type** dropdown
### Via API
```bash
curl -X POST {{API_URL}}/templates \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Password Reset",
"type": "TRANSACTIONAL",
"subject": "Reset your password",
"body": "<p>Click: {{resetLink}}</p>"
}'
```
Type: `MARKETING` or `TRANSACTIONAL`
## Changing type
Update anytime:
```bash
curl -X PATCH {{API_URL}}/templates/template_id \
-H "Authorization: Bearer sk_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"type": "MARKETING"}'
```
**Warning:** Changing transactional → marketing stops sending to unsubscribed contacts.
## Unsubscribe links
Marketing templates auto-include unsubscribe link in footer.
Custom placement:
```html
<a href="{{unsubscribeUrl}}">Unsubscribe</a>
```
Transactional templates don't need unsubscribe links.
## Choosing the right type
Ask: "Would user be frustrated if they didn't receive this after unsubscribing?"
**If yes → Transactional**
- Password resets
- Order confirmations
- Account alerts
**If no → Marketing**
- Newsletters
- Product announcements
- Promotions
## Best practices
**Default to marketing** — Only use transactional for truly necessary emails.
**Don't abuse transactional** — Sending marketing content via transactional templates violates regulations and damages reputation.
**Test both states** — Verify behavior with subscribed and unsubscribed contacts.
## Next steps
- [Create templates](/guides/templates)
- [Manage subscriptions](/concepts/contacts-and-data)
- [Send emails](/tutorials/first-transactional-email)