"
- }'
-```
-
-The `firstName` and `plan` values come from the contact's `data` field.
-
-### Fallback values
-
-Provide defaults when data might be missing:
-
-```html
-
Hello {{firstName ?? 'there'}}!
-
Plan: {{plan ?? 'Free'}}
-```
-
-If `firstName` is not set, displays "Hello there!" instead of blank.
-
-### Passing additional data
-
-Send extra data for a specific email without saving it to the contact:
-
-```bash
-curl -X POST {{API_URL}}/v1/send \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "to": "user@example.com",
- "subject": "Your verification code",
- "body": "
Your code: {{verificationCode}}
",
- "data": {
- "verificationCode": "ABC123"
- }
- }'
-```
-
-The `verificationCode` is used in the email but not saved to the contact. This is useful for:
-- One-time codes (password reset, verification)
-- Session-specific data
-- Temporary discount codes
-- Order-specific details
-
-### Reserved variables
-
-These are always available in templates:
-
-- `{{email}}` — Contact email address
-- `{{id}}` — Contact ID
-
-Example:
-```html
-
-```
-
-## Listing contacts
-
-### Get all contacts
-
-```bash
-curl -X GET "{{API_URL}}/contacts?limit=50" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Returns:
-
-```json
-{
- "success": true,
- "data": {
- "items": [...],
- "nextCursor": "abc123",
- "hasMore": true,
- "total": 10000
- }
-}
-```
-
-### Pagination
-
-For large lists, use cursor-based pagination:
-
-```javascript
-let allContacts = [];
-let cursor = null;
-
-do {
- const params = new URLSearchParams({ limit: 100 });
- if (cursor) params.append('cursor', cursor);
-
- const response = await fetch(`{{API_URL}}/contacts?${params}`, {
- headers: { 'Authorization': `Bearer ${PLUNK_SECRET_KEY}` }
- });
-
- const { data } = await response.json();
- allContacts.push(...data.items);
- cursor = data.nextCursor;
-} while (cursor);
-```
-
-### Filter by subscription
-
-```bash
-# Only subscribed
-curl -X GET "{{API_URL}}/contacts?subscribed=true" \
- -H "Authorization: Bearer sk_your_secret_key"
-
-# Only unsubscribed
-curl -X GET "{{API_URL}}/contacts?subscribed=false" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### Search by email
-
-```bash
-curl -X GET "{{API_URL}}/contacts?search=sarah" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Searches for emails containing "sarah".
-
-## Getting a contact
-
-### By ID
-
-```bash
-curl -X GET {{API_URL}}/contacts/contact_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-## Updating contacts
-
-### Update contact data
-
-```bash
-curl -X PATCH {{API_URL}}/contacts/contact_id \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "subscribed": true,
- "data": {
- "plan": "premium",
- "mrr": 99
- }
- }'
-```
-
-### Data merging
-
-Updates merge with existing data:
-
-```javascript
-// Current contact data
-{ "firstName": "Sarah", "company": "Acme" }
-
-// Update with
-{ "lastName": "Chen", "plan": "pro" }
-
-// Result
-{ "firstName": "Sarah", "company": "Acme", "lastName": "Chen", "plan": "pro" }
-```
-
-To remove a field, set it to `null`.
-
-### Change subscription status
-
-```bash
-curl -X PATCH {{API_URL}}/contacts/contact_id \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{"subscribed": false}'
-```
-
-**Marketing templates** only send to subscribed contacts. **Transactional templates** send to everyone, regardless of subscription status.
-
-## Deleting contacts
-
-```bash
-curl -X DELETE {{API_URL}}/contacts/contact_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-**Warning:** Deletion is permanent. Consider unsubscribing instead of deleting.
-
-## Bulk operations
-
-### Import from CSV
-
-Prepare a CSV file:
-
-```csv
-email,firstName,lastName,plan
-sarah@example.com,Sarah,Chen,pro
-john@example.com,John,Doe,free
-```
-
-Upload via dashboard:
-1. Go to **Contacts**
-2. Click **Import CSV**
-3. Upload file
-4. Map columns
-5. Set default subscription status
-6. Import
-
-The import runs in the background. You'll receive a summary when complete.
-
-## Available fields
-
-### Get all custom fields
-
-See what data fields your contacts have:
-
-```bash
-curl -X GET {{API_URL}}/contacts/fields \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Returns unique field names across all contacts:
-
-```json
-{
- "fields": [
- "firstName",
- "lastName",
- "company",
- "plan",
- "mrr",
- "signupDate"
- ]
-}
-```
-
-### Get field values
-
-See all unique values for a specific field:
-
-```bash
-curl -X GET {{API_URL}}/contacts/fields/plan/values \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Returns:
-
-```json
-{
- "values": ["free", "pro", "premium", "enterprise"]
-}
-```
-
-Useful for building segment filters and understanding your data.
-
-## Syncing with your app
-
-Keep contacts in sync with your user database:
-
-```javascript
-// When user signs up
-async function onUserSignup(user) {
- await fetch('{{API_URL}}/contacts', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${PLUNK_SECRET_KEY}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- email: user.email,
- subscribed: true,
- data: {
- firstName: user.firstName,
- lastName: user.lastName,
- signupDate: new Date().toISOString()
- }
- })
- });
-}
-
-// When user updates profile
-async function onUserUpdate(user) {
- await fetch(`{{API_URL}}/contacts/${user.contactId}`, {
- method: 'PATCH',
- headers: {
- 'Authorization': `Bearer ${PLUNK_SECRET_KEY}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- data: {
- firstName: user.firstName,
- lastName: user.lastName,
- company: user.company
- }
- })
- });
-}
-
-// When user subscribes to plan
-async function onSubscriptionChange(user, plan, mrr) {
- await fetch(`{{API_URL}}/contacts/${user.contactId}`, {
- method: 'PATCH',
- headers: {
- 'Authorization': `Bearer ${PLUNK_SECRET_KEY}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- data: {
- plan,
- mrr,
- subscriptionDate: new Date().toISOString()
- }
- })
- });
-}
-```
-
-## Best practices
-
-**Sync critical data only** — Don't sync every field. Focus on data used in segments, workflows, and personalization.
-
-**Use webhooks for real-time sync** — Update contacts immediately when user data changes.
-
-**Track subscription separately** — Use the `subscribed` field for email preferences, not app subscription status.
-
-**Clean your list regularly** — Remove or unsubscribe bounced and inactive contacts.
-
-**Respect opt-outs** — When users unsubscribe, update immediately. Don't re-subscribe them automatically.
-
-**Test with real emails** — Use your own email addresses to test the contact experience.
-
-## Next Steps
-
-- [Build segments](/guides/segments) to group contacts
-- [Send campaigns](/guides/campaigns) to your contacts
-- [Track events](/guides/events) to update contact data automatically
diff --git a/apps/wiki/content/docs/guides/custom-domains.mdx b/apps/wiki/content/docs/guides/custom-domains.mdx
deleted file mode 100644
index 06d5692..0000000
--- a/apps/wiki/content/docs/guides/custom-domains.mdx
+++ /dev/null
@@ -1,209 +0,0 @@
----
-title: Custom Domains
-description: Send emails from your own domain
----
-
-## Why use custom domains
-
-Sending from your own domain (e.g., `hello@yourdomain.com`) instead of a shared domain:
-
-- **Better deliverability** — Email providers trust emails from verified domains
-- **Brand consistency** — Recipients see your brand, not Plunk
-- **Higher trust** — Your domain builds its own sender reputation
-- **Professional appearance** — Custom addresses look more legitimate
-
-## Requirements
-
-- **Domain ownership** — You own or control the domain
-- **DNS access** — Ability to add DNS records
-- **Verification** — Add DKIM records to prove ownership
-
-## Adding a domain
-
-1. Go to **Settings > Domains**
-2. Click **Add Domain**
-3. Enter your domain (e.g., `yourdomain.com`)
-4. Copy the provided DNS records
-
-## DNS configuration
-
-After adding your domain, you'll receive 3 DKIM tokens. Add them as CNAME records to your DNS.
-
-### Common DNS providers
-
-#### Cloudflare
-
-1. Log into Cloudflare
-2. Select your domain
-3. Go to **DNS > Records**
-4. Click **Add record**
-5. Select **CNAME** type
-6. Paste name and value from Plunk
-7. Click **Save**
-8. Repeat for all 3 records
-
-#### Namecheap
-
-1. Log into Namecheap
-2. Go to **Domain List**
-3. Click **Manage** next to your domain
-4. Select **Advanced DNS**
-5. Click **Add New Record**
-6. Choose **CNAME Record**
-7. Enter host and value
-8. Repeat for all 3 records
-
-#### GoDaddy
-
-1. Log into GoDaddy
-2. Go to **My Products**
-3. Click **DNS** next to your domain
-4. Click **Add** under Records
-5. Select **CNAME** type
-6. Enter name and value
-7. Repeat for all 3 records
-
-#### Route 53 (AWS)
-
-1. Open Route 53 console
-2. Select your hosted zone
-3. Click **Create record**
-4. Enter record name
-5. Select **CNAME** type
-6. Paste value
-7. Create record
-8. Repeat for all 3 records
-
-## Verification
-
-### Automatic verification
-
-Plunk checks DNS records every 5 minutes automatically. Verification typically completes within 10-30 minutes after adding DNS records.
-
-**Note:** DNS propagation can take up to 48 hours, though it's usually much faster.
-
-Check verification status in your dashboard at **Settings > Domains**.
-
-## Using your domain
-
-Once verified, specify your domain in the `from` field:
-
-### In transactional emails
-
-```bash
-curl -X POST {{API_URL}}/v1/send \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "to": "customer@example.com",
- "from": "orders@yourdomain.com",
- "fromName": "Your Company",
- "subject": "Order confirmed",
- "body": "
Your order has been confirmed.
"
- }'
-```
-
-### In templates
-
-Set default from address in template:
-
-```bash
-curl -X POST {{API_URL}}/templates \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Order Confirmation",
- "subject": "Order #{{orderNumber}} confirmed",
- "body": "...",
- "from": "orders@yourdomain.com",
- "fromName": "Your Company",
- "type": "TRANSACTIONAL"
- }'
-```
-
-### In campaigns
-
-Campaigns use the template's from address, or you can override:
-
-```bash
-curl -X POST {{API_URL}}/campaigns \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Newsletter",
- "templateId": "template_id",
- "from": "newsletter@yourdomain.com",
- "audienceType": "ALL"
- }'
-```
-
-## Managing domains
-
-### Remove domain
-
-Go to **Settings > Domains**, select the domain, and click **Remove**.
-
-**Warning:** Emails using this domain will fail to send after removal.
-
-## Troubleshooting
-
-### Domain won't verify
-
-**1. Check DNS records are correct**
-
-Verify records are added exactly as provided
-
-**2. Wait for propagation**
-
-DNS changes can take up to 48 hours to propagate globally. Check periodically.
-
-**3. Remove conflicting records**
-
-If you previously used another email service, remove their DKIM records to avoid conflicts.
-
-**4. Check for typos**
-
-Ensure record names and values match exactly. Common issues:
-- Extra spaces in values
-- Missing dots in record names
-- Wrong subdomain
-
-### Emails not sending from domain
-
-**1. Verify domain is verified** - Check status in **Settings > Domains**.
-
-**2. Use correct email format**
-
-Must be `username@yourdomain.com`, not `@subdomain.yourdomain.com`.
-
-**3. Check sender reputation**
-
-New domains have no reputation. Start with small volumes and gradually increase.
-
-### Emails going to spam
-
-After adding custom domain:
-
-**1. Warm up your domain** — See [Scaling Email](/guides/scaling-email)
-
-**2. Monitor deliverability** — Check [Analytics](/guides/analytics) for bounce/complaint rates
-
-**3. Clean your list** — Remove bounced addresses immediately
-
-## Best practices
-
-**Start small** — Send to engaged users first to build reputation.
-
-**Monitor metrics** — Watch bounce and complaint rates closely.
-
-**Use subdomains** — Consider `mail.yourdomain.com` for email to separate from main domain reputation.
-
-**Keep DNS records** — Don't remove DKIM records even if verification is complete.
-
-**Test thoroughly** — Send test emails to various providers (Gmail, Outlook, Yahoo).
-
-## Next Steps
-
-- [Scale email delivery](/guides/scaling-email) with your custom domain
-- [Monitor analytics](/guides/analytics) for domain performance
-- [Troubleshoot issues](/guides/troubleshooting) if problems arise
diff --git a/apps/wiki/content/docs/guides/email-attachments.mdx b/apps/wiki/content/docs/guides/email-attachments.mdx
deleted file mode 100644
index 6b6e61d..0000000
--- a/apps/wiki/content/docs/guides/email-attachments.mdx
+++ /dev/null
@@ -1,385 +0,0 @@
----
-title: Email Attachments
-description: Send emails with file attachments via API or SMTP
----
-
-## Overview
-
-Plunk supports sending emails with file attachments through both the HTTP API and SMTP relay. You can attach documents, images, PDFs, and other files to your transactional emails.
-
-## Limits
-
-- **Maximum attachments**: 10 per email
-- **Total size limit**: 10MB (combined size of all attachments)
-- **Supported formats**: Any file type (PDF, images, documents, etc.)
-
-## API Usage
-
-### Basic Example
-
-Send an email with a single PDF attachment:
-
-```bash
-curl -X POST https://api.useplunk.com/v1/send \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "to": "user@example.com",
- "subject": "Your Invoice",
- "body": "
",
- "attachments": [
- {
- "filename": "getting-started-guide.pdf",
- "content": "JVBERi0xLjQK...",
- "contentType": "application/pdf"
- },
- {
- "filename": "sample-data.csv",
- "content": "TmFtZSxFbWFp...",
- "contentType": "text/csv"
- }
- ]
-}
-```
-
-## Next Steps
-
-- [Send your first email](/getting-started/quick-start)
-- [SMTP relay setup](/self-hosting/introduction)
-- [Email templates](/guides/templates)
diff --git a/apps/wiki/content/docs/guides/events.mdx b/apps/wiki/content/docs/guides/events.mdx
deleted file mode 100644
index 8e3480e..0000000
--- a/apps/wiki/content/docs/guides/events.mdx
+++ /dev/null
@@ -1,223 +0,0 @@
----
-title: Events
-description: Track user actions and behavior
----
-
-## What are events
-
-Events track user actions in your application. Use them to:
-- Trigger automated workflows
-- Build behavior-based segments
-- Analyze user engagement
-- Track conversion funnels
-
-Common events: signups, purchases, logins, feature usage, page views.
-
-## Tracking events
-
-Use your **public key** for event tracking:
-
-```javascript
-fetch('{{API_URL}}/v1/track', {
- method: 'POST',
- headers: {
- 'Authorization': 'Bearer pk_your_public_key',
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- email: 'user@example.com',
- event: 'button_clicked',
- data: {
- button: 'signup',
- page: '/pricing'
- }
- })
-});
-```
-
-This creates or updates the contact and tracks the event.
-
-### Persistent vs. Non-Persistent Data
-
-Event data can be either **persistent** (saved to contact) or **non-persistent** (available only to workflows):
-
-**Simple values are persistent** — Saved to contact profile:
-```javascript
-{
- email: 'user@example.com',
- event: 'subscription_created',
- data: {
- plan: 'premium', // Saved to contact.data.plan
- mrr: 99.00 // Saved to contact.data.mrr
- }
-}
-```
-
-**Non-persistent values** — Available only to triggered workflows:
-```javascript
-{
- email: 'user@example.com',
- event: 'order_placed',
- data: {
- totalSpent: 299.99, // Persistent - saved to contact
- orderId: {value: 'order-12345', persistent: false}, // Non-persistent - workflows only
- receiptUrl: {value: 'https://...', persistent: false} // Non-persistent - workflows only
- }
-}
-```
-
-**Why use non-persistent data?**
-- Temporary tokens/codes (password reset, verification)
-- One-time URLs or session data
-- Data that shouldn't pollute contact profiles
-- Information needed only for a specific workflow
-
-Non-persistent data is available throughout the entire workflow execution but never stored on the contact record.
-
-## Event structure
-
-Each event stores:
-
-```json
-{
- "id": "evt_abc123",
- "name": "purchase",
- "contactId": "contact_xyz",
- "data": {
- "product": "Premium Plan",
- "amount": 99.00
- },
- "createdAt": "2024-03-15T10:30:00Z"
-}
-```
-
-## Using events
-You can use events to trigger workflows, create segments, and analyze user behavior.
-
-## Common event patterns
-
-### Lifecycle events
-
-```javascript
-// User signs up
-track({ email, event: 'signed_up', data: { source: 'homepage' } });
-
-// User activates account
-track({ email, event: 'account_activated' });
-
-// User completes onboarding
-track({ email, event: 'onboarding_completed', data: { steps: 5 } });
-```
-
-### Commerce events
-
-```javascript
-// Add to cart
-track({ email, event: 'cart_added', data: { productId: '123', price: 49 } });
-
-// Purchase
-track({ email, event: 'purchase', data: { orderId: '456', total: 99 } });
-
-// Subscription created
-track({ email, event: 'subscription_created', data: { plan: 'pro', mrr: 29 } });
-```
-
-### Engagement events
-
-```javascript
-// Feature used
-track({ email, event: 'feature_used', data: { feature: 'export' } });
-
-// Page viewed
-track({ email, event: 'page_view', data: { path: '/dashboard' } });
-
-// Login
-track({ email, event: 'logged_in' });
-```
-
-### Automatic events
-
-Plunk sends these automatically:
-
-**Email events:**
-- `email.sent` — Email delivered to inbox
-- `email.opened` — Email opened (first time)
-- `email.clicked` — Link clicked in email
-- `email.bounced` — Email bounced
-- `email.complained` — Spam complaint
-
-**Segment events** (if membership tracking enabled):
-- `segment.entered` — Contact joined segment
-- `segment.exited` — Contact left segment
-
-## Event naming conventions
-
-**Use lowercase with underscores:**
-```
-✓ user_signed_up
-✓ purchase_completed
-✗ UserSignedUp
-✗ purchaseCompleted
-```
-
-**Be specific but concise:**
-```
-✓ trial_started
-✓ subscription_cancelled
-✗ user_started_a_trial
-✗ sub_cancel
-```
-
-**Group related events:**
-```
-user_signed_up
-user_logged_in
-user_deleted_account
-
-subscription_created
-subscription_renewed
-subscription_cancelled
-```
-
-## Managing events
-
-### List events
-
-```bash
-curl -X GET "{{API_URL}}/events?limit=100" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### List unique event names
-
-```bash
-curl -X GET {{API_URL}}/events/names \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Returns all event names tracked in your project.
-
-### Get events for a contact
-
-```bash
-curl -X GET {{API_URL}}/events?contactId=contact_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-## Best practices
-
-**Track meaningful actions** — Focus on events that indicate intent or value (signups, purchases, key features).
-
-**Include context** — Add relevant data to understand the event better (product, amount, source).
-
-**Be consistent** — Use the same event names and data structure across your application.
-
-**Don't over-track** — Tracking every click creates noise. Focus on conversion events and key milestones.
-
-**Test your tracking** — Verify events appear in dashboard before building workflows around them.
-
-## What's next
-
-- [Build workflows](/guides/workflows) triggered by events
-- [Create segments](/guides/segments) based on event data
-- [Analyze events](/guides/analytics) to understand user behavior
diff --git a/apps/wiki/content/docs/guides/meta.json b/apps/wiki/content/docs/guides/meta.json
deleted file mode 100644
index cd43e7a..0000000
--- a/apps/wiki/content/docs/guides/meta.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "title": "Guides",
- "pages": [
- "contacts",
- "templates",
- "campaigns",
- "segments",
- "workflows",
- "events",
- "webhooks",
- "analytics",
- "custom-domains",
- "billing-limits",
- "scaling-email",
- "troubleshooting"
- ]
-}
diff --git a/apps/wiki/content/docs/guides/request-ids.mdx b/apps/wiki/content/docs/guides/request-ids.mdx
deleted file mode 100644
index 72e9e3a..0000000
--- a/apps/wiki/content/docs/guides/request-ids.mdx
+++ /dev/null
@@ -1,307 +0,0 @@
----
-title: Request IDs & Debugging
-description: How to use request IDs for debugging and tracing API requests
----
-
-## Overview
-
-Every API request to Plunk receives a unique request ID that follows the request through the entire system. Request IDs are essential for debugging, support, and monitoring.
-
-## What are Request IDs?
-
-A request ID is a UUID (e.g., `f47ac10b-58cc-4372-a567-0e02b2c3d479`) that:
-- Is generated for every API request
-- Appears in all related log entries
-- Is included in both success and error responses
-- Can be used to trace requests across services
-
-## Where to Find Request IDs
-
-### In API Responses
-
-**Error responses** (in the `error.requestId` field):
-```json
-{
- "success": false,
- "error": {
- "code": "VALIDATION_ERROR",
- "message": "Request validation failed",
- "requestId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
- ...
- }
-}
-```
-
-**Response headers** (always present, even on success):
-```bash
-X-Request-ID: f47ac10b-58cc-4372-a567-0e02b2c3d479
-```
-
-### In Your Application
-
-You can capture and log request IDs for correlation:
-
-```javascript
-const response = await fetch('https://api.useplunk.com/v1/send', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ to, subject, body })
-});
-
-// Get request ID from response header
-const requestId = response.headers.get('X-Request-ID');
-
-// Log it for correlation
-console.log(`[${requestId}] Email send request initiated`);
-
-const data = await response.json();
-if (!data.success) {
- // Request ID is also in error response
- console.error(`[${data.error.requestId}] Error:`, data.error.message);
-}
-```
-
-## Database Request Logging
-
-In addition to console/file logs, Plunk stores all API requests in the database with their request IDs. This provides:
-
-- **Historical audit trail** - See all API calls made to your project
-- **Analytics** - Analyze API usage patterns, error rates, popular endpoints
-- **User-facing logs** - Display API request history in your dashboard
-- **Long-term debugging** - Investigate issues that occurred days or weeks ago
-- **Compliance** - Meet audit requirements for API access logs
-
-### Database Schema
-
-Each request is stored with:
-- Request ID (primary key)
-- HTTP method and path
-- Status code and response time
-- Project ID and user ID (if authenticated)
-- IP address and user agent
-- Error code and message (if failed)
-- Request/response sizes
-- Timestamp
-
-### Retention Policy
-
-API request logs are retained for **30 days** by default. A background job runs daily at 3 AM to delete older logs. This prevents unbounded table growth while maintaining recent history for debugging.
-
-You can query your request logs via SQL if self-hosting:
-
-```sql
--- Find all failed requests in the last 24 hours
-SELECT * FROM api_requests
-WHERE "statusCode" >= 400
-AND "createdAt" > NOW() - INTERVAL '24 hours'
-ORDER BY "createdAt" DESC;
-
--- Find all requests for a specific project
-SELECT * FROM api_requests
-WHERE "projectId" = 'prj_abc123'
-ORDER BY "createdAt" DESC
-LIMIT 100;
-
--- Analyze error rates by endpoint
-SELECT
- path,
- COUNT(*) as total_requests,
- COUNT(*) FILTER (WHERE "statusCode" >= 400) as errors,
- ROUND(100.0 * COUNT(*) FILTER (WHERE "statusCode" >= 400) / COUNT(*), 2) as error_rate_pct
-FROM api_requests
-WHERE "createdAt" > NOW() - INTERVAL '7 days'
-GROUP BY path
-ORDER BY error_rate_pct DESC;
-```
-
-## How Request IDs Help with Debugging
-
-### Example Scenario
-
-You send an email via the API and receive an error. Here's how request IDs help:
-
-**1. Your application receives an error:**
-```json
-{
- "success": false,
- "error": {
- "code": "TEMPLATE_NOT_FOUND",
- "message": "Template with ID \"tpl_abc123\" was not found",
- "requestId": "a1b2c3d4-e5f6-7890-gh12-i34567890jkl",
- ...
- }
-}
-```
-
-**2. You contact support with the request ID**
-
-**3. We search our logs for that request ID and see:**
-
-```
-[a1b2c3d4-e5f6-7890-gh12-i34567890jkl] POST /v1/send → Request received
- └─ authType: apiKey
- └─ projectId: prj_xyz789
- └─ ip: 192.168.1.1
-
-[a1b2c3d4-e5f6-7890-gh12-i34567890jkl] Looking up template: tpl_abc123
- └─ projectId: prj_xyz789
-
-[a1b2c3d4-e5f6-7890-gh12-i34567890jkl] Template not found
- └─ errorCode: TEMPLATE_NOT_FOUND
- └─ statusCode: 404
-
-[a1b2c3d4-e5f6-7890-gh12-i34567890jkl] POST /v1/send → 404 (45ms)
-```
-
-From this, we can immediately see:
-- You're authenticated correctly (authType: apiKey)
-- The template ID doesn't exist in your project
-- The request took 45ms to process
-- No database errors or system issues
-
-**Result:** We can quickly tell you "That template doesn't exist in your project" without back-and-forth debugging.
-
-## Using Request IDs in Self-Hosted Deployments
-
-If you're self-hosting Plunk, you can use request IDs to debug issues in your own logs.
-
-### Searching Logs
-
-**With Docker logs:**
-```bash
-# Find all logs for a specific request
-docker logs plunk-api 2>&1 | grep "a1b2c3d4-e5f6-7890-gh12-i34567890jkl"
-```
-
-**With standard logs:**
-```bash
-# Search application logs
-grep "a1b2c3d4-e5f6-7890-gh12-i34567890jkl" /var/log/plunk/api.log
-
-# Search with context (10 lines before and after)
-grep -C 10 "a1b2c3d4-e5f6-7890-gh12-i34567890jkl" /var/log/plunk/api.log
-```
-
-### Log Structure
-
-Every log entry includes the request ID in brackets:
-
-```
-[f47ac10b-58cc-4372-a567-0e02b2c3d479] POST /v1/track → Request received
-[f47ac10b-58cc-4372-a567-0e02b2c3d479] Contact created: cnt_abc123
-[f47ac10b-58cc-4372-a567-0e02b2c3d479] Event tracked: evt_xyz789
-[f47ac10b-58cc-4372-a567-0e02b2c3d479] POST /v1/track → 200 (127ms)
-```
-
-This makes it easy to trace a single request from start to finish.
-
-## Providing Request IDs with Load Balancers
-
-If you use a load balancer or API gateway, you can pass your own request IDs:
-
-```bash
-curl -X POST https://api.useplunk.com/v1/send \
- -H "X-Request-ID: your-custom-request-id" \
- -H "Authorization: Bearer sk_..." \
- -H "Content-Type: application/json" \
- -d '...'
-```
-
-Plunk will use your provided request ID instead of generating a new one. This allows you to:
-- Correlate requests across your entire system
-- Trace requests from your frontend → your backend → Plunk → email delivery
-- Maintain consistent request IDs in your monitoring tools
-
-## Best Practices
-
-### 1. Always Log Request IDs
-
-```javascript
-// ✅ Good: Log request ID for correlation
-const response = await plunk.send(email);
-const requestId = response.headers.get('X-Request-ID');
-logger.info(`Email sent to ${email.to}`, { requestId });
-```
-
-```javascript
-// ❌ Bad: Discard request ID
-await plunk.send(email);
-// No way to correlate this with Plunk's logs
-```
-
-### 2. Include in Error Reporting
-
-```javascript
-// ✅ Good: Include request ID in error reports
-try {
- await plunk.send(email);
-} catch (error) {
- Sentry.captureException(error, {
- extra: {
- requestId: error.requestId,
- emailTo: email.to
- }
- });
-}
-```
-
-### 3. Store for Audit Trails
-
-```javascript
-// ✅ Good: Store request ID in your database
-await db.emailLog.create({
- to: email.to,
- subject: email.subject,
- plunkRequestId: requestId,
- sentAt: new Date()
-});
-```
-
-### 4. Return to End Users (Optional)
-
-For customer-facing applications, you can show request IDs to users:
-
-```
-❌ Error sending email. Please try again.
-```
-
-```
-❌ Error sending email. Please contact support and provide this reference: a1b2c3d4-e5f6
-```
-
-## Monitoring and Observability
-
-Request IDs are essential for:
-
-- **Distributed tracing** - Follow requests across services
-- **Error correlation** - Link errors to specific API calls
-- **Performance monitoring** - Identify slow requests
-- **Debugging production** - Reproduce issues without PII
-- **Rate limit tracking** - Monitor usage patterns per project
-
-## FAQ
-
-### Do request IDs expire?
-
-No, request IDs are logged indefinitely (subject to your log retention policy).
-
-### Can I reuse request IDs?
-
-No, each request should have a unique ID. If you send the same request ID twice, logs will be mixed.
-
-### Are request IDs sequential?
-
-No, they are random UUIDs. This prevents information leakage about request volume.
-
-### Can I search by request ID in the dashboard?
-
-This feature is planned but not yet available. For now, contact support with the request ID.
-
-## Related Documentation
-
-- [Error Codes](/api-reference/errors) - Understanding API errors
-- [API Reference](/api-reference/overview) - Complete API documentation
-- [Troubleshooting](/guides/troubleshooting) - Common issues and solutions
diff --git a/apps/wiki/content/docs/guides/scaling-email.mdx b/apps/wiki/content/docs/guides/scaling-email.mdx
deleted file mode 100644
index f93c452..0000000
--- a/apps/wiki/content/docs/guides/scaling-email.mdx
+++ /dev/null
@@ -1,277 +0,0 @@
----
-title: Scaling Email Delivery
-description: Best practices for high-volume email sending
----
-
-## Email delivery at scale
-
-Plunk is built on AWS SES and handles millions of emails. Follow these practices to maintain high deliverability and performance at scale.
-
-## Deliverability best practices
-
-### Use custom domains
-
-Emails from custom domains have higher trust and better deliverability than shared domains.
-
-**Setup:**
-1. Go to **Settings > Domains**
-2. Add your domain
-3. Configure DNS records (DKIM, SPF)
-4. Wait for verification
-
-[Learn more about custom domains →](/guides/custom-domains)
-
-### Warm up new domains
-
-Start small and gradually increase volume.
-
-This builds sender reputation with email providers.
-
-### Clean your list regularly
-
-Remove bounced and inactive contacts:
-
-```javascript
-// Get bounced contacts
-const bounced = await fetch('{{API_URL}}/events?name=email.bounced&limit=1000', {
- headers: { 'Authorization': `Bearer ${apiKey}` }
-});
-
-// Unsubscribe them
-for (const event of bounced.data.events) {
- await fetch(`{{API_URL}}/contacts/${event.contactId}`, {
- method: 'PATCH',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ subscribed: false })
- });
-}
-```
-
-**When to clean:**
-- Hard bounces: Immediately unsubscribe
-- Soft bounces: After 3 attempts
-- No engagement: After 6-12 months
-
-### Segment your audience
-
-Send relevant content to engaged users:
-
-```javascript
-// Create engaged users segment
-{
- "name": "Engaged Users",
- "filters": {
- "operator": "OR",
- "conditions": [
- { "field": "data.lastOpenedAt", "operator": "greaterThan", "value": "{{90 days ago}}" },
- { "field": "data.lastClickedAt", "operator": "greaterThan", "value": "{{90 days ago}}" }
- ]
- }
-}
-```
-
-Send campaigns to engaged segments for better rates.
-
-## Tracking control
-
-You can disable tracking in your project settings for privacy-focused audiences or to reduce email size.
-
-**When tracking is disabled:**
-- No tracking pixel (no open tracking)
-- Links not rewritten (no click tracking)
-- Smaller email size
-- May improve deliverability for privacy-conscious audiences
-
-**When to disable:**
-- Regulated industries (healthcare, finance)
-- Privacy-focused users
-- Transactional emails where tracking isn't needed
-- High-volume sends where analytics aren't critical
-
-## Rate limits
-
-### AWS SES limits
-
-Plunk automatically queues emails to stay within limits. Large sends process in background.
-
-### Increase limits
-
-For higher throughput:
-1. Maintain good sender reputation
-2. Consistent sending volume
-3. Low bounce/complaint rates
-4. Request limit increase from AWS
-
-### Batch operations
-
-For bulk operations, use appropriate endpoints:
-
-```javascript
-// ✓ Good: Single request for multiple recipients
-fetch('{{API_URL}}/v1/send', {
- method: 'POST',
- body: JSON.stringify({
- to: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
- subject: 'Update',
- body: 'Message'
- })
-});
-
-// ✗ Avoid: Multiple requests
-for (const email of emails) {
- await fetch('{{API_URL}}/v1/send', {...}); // Sequential, slow
-}
-```
-
-## Campaign targeting
-
-### Dynamic filtering
-
-Target audiences without creating segments:
-
-```javascript
-{
- "name": "Premium Launch",
- "audienceType": "FILTERED",
- "audienceFilter": {
- "operator": "AND",
- "conditions": [
- { "field": "data.plan", "operator": "equals", "value": "premium" },
- { "field": "data.signupDate", "operator": "greaterThan", "value": "2025-01-01" },
- { "field": "subscribed", "operator": "equals", "value": true }
- ]
- },
- "templateId": "template_id"
-}
-```
-
-**Use FILTERED for:**
-- One-time sends
-- Testing targeting
-- Very specific criteria
-
-**Use SEGMENT for:**
-- Repeated targeting
-- Workflow triggers
-- Segment analytics
-
-## Segment membership tracking
-
-For segments used in workflows, enable membership tracking:
-
-```bash
-curl -X POST {{API_URL}}/segments \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Active Premium Users",
- "filters": {...},
- "trackMembership": true
- }'
-```
-
-**Enable for:**
-- Workflow trigger segments
-- Lifecycle stage tracking
-- Cohort analysis
-
-**Disable for:**
-- Large segments (100k+ contacts)
-- Campaign-only segments
-- Frequently changing segments
-
-Membership updates run every 5 minutes in background.
-
-## Performance optimization
-
-### Cache contact data
-
-For high-volume API sends, cache contact lookups:
-
-```javascript
-// Cache contact IDs locally
-const contactCache = new Map();
-
-async function getContactId(email) {
- if (contactCache.has(email)) {
- return contactCache.get(email);
- }
-
- const contact = await fetch(`{{API_URL}}/contacts?search=${email}`);
- contactCache.set(email, contact.id);
- return contact.id;
-}
-```
-
-### Use webhooks for async processing
-
-Instead of waiting for email sends:
-
-```javascript
-// Workflow with webhook for confirmation
-{
- "steps": [
- { "type": "SEND_EMAIL", "config": {...} },
- {
- "type": "WEBHOOK",
- "config": {
- "url": "https://your-api.com/email-sent",
- "method": "POST",
- "body": {
- "contactId": "{{id}}",
- "emailId": "{{emailId}}"
- }
- }
- }
- ]
-}
-```
-
-### Batch workflow triggers
-
-Trigger workflows in batches instead of one at a time:
-
-```javascript
-// Batch event tracking
-const events = users.map(user => ({
- email: user.email,
- event: 'welcome_campaign',
- data: { userId: user.id }
-}));
-
-// Send in parallel (respecting rate limits)
-await Promise.all(
- events.map(event =>
- fetch('{{API_URL}}/v1/track', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(event)
- })
- )
-);
-```
-
-## Monitoring
-
-### Track key metrics
-
-Monitor these regularly:
-
-- **Bounce rate** — Should be < 2%
-- **Complaint rate** — Should be < 0.1%
-- **Open rate** — Industry average 15-25%
-- **Click rate** — Industry average 2-5%
-
-Monitor these metrics in your dashboard analytics page to track deliverability and engagement over time.
-
-## Next Steps
-
-- [Set billing limits](/guides/billing-limits) to control costs
-- [Monitor analytics](/guides/analytics) for deliverability
-- [Troubleshoot issues](/guides/troubleshooting) if problems arise
diff --git a/apps/wiki/content/docs/guides/segments.mdx b/apps/wiki/content/docs/guides/segments.mdx
deleted file mode 100644
index 8b4f37e..0000000
--- a/apps/wiki/content/docs/guides/segments.mdx
+++ /dev/null
@@ -1,342 +0,0 @@
----
-title: Segments
-description: Create dynamic audience groups with filters
----
-
-## What are segments
-
-Segments are dynamic groups of contacts based on filter conditions. Unlike static lists, segments automatically update as contact data changes.
-
-Use segments to:
-- Target specific audiences in campaigns
-- Trigger workflows when contacts enter/exit
-- Analyze cohorts and user behavior
-- Personalize communications
-
-## Creating segments
-
-### In the dashboard
-
-1. Go to **Segments**
-2. Click **Create Segment**
-3. Name your segment
-4. Add filter conditions
-5. Enable membership tracking (optional)
-6. Save
-
-### Via API
-
-```bash
-curl -X POST {{API_URL}}/segments \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Active Premium Users",
- "filters": {
- "operator": "AND",
- "conditions": [
- {
- "field": "data.plan",
- "operator": "equals",
- "value": "premium"
- },
- {
- "field": "subscribed",
- "operator": "equals",
- "value": true
- }
- ]
- }
- }'
-```
-
-## Filter conditions
-
-Combine conditions with `AND` or `OR` operators to build precise segments.
-
-### Available operators
-
-**Equality**
-- `equals` — Exact match
-- `notEquals` — Does not match
-
-**Text**
-- `contains` — String includes value
-- `notContains` — String excludes value
-
-**Numeric/Date**
-- `greaterThan` — Larger than value
-- `lessThan` — Smaller than value
-- `greaterThanOrEquals` — At least value
-- `lessThanOrEquals` — At most value
-
-**Arrays**
-- `in` — Value in array
-- `notIn` — Value not in array
-
-**Existence**
-- `exists` — Field has a value
-- `notExists` — Field is missing or null
-
-### Field paths
-
-Access contact fields with dot notation:
-
-- `email` — Contact email address
-- `subscribed` — Subscription status
-- `createdAt` — When contact was created
-- `data.firstName` — Custom field
-- `data.preferences.newsletter` — Nested field
-- `data.lastPurchaseDate` — Custom date field
-
-## Example segments
-
-### Premium subscribers
-
-All contacts on premium plan who are subscribed:
-
-```json
-{
- "operator": "AND",
- "conditions": [
- { "field": "data.plan", "operator": "equals", "value": "premium" },
- { "field": "subscribed", "operator": "equals", "value": true }
- ]
-}
-```
-
-### Recent signups
-
-Contacts who joined in the last 7 days:
-
-```json
-{
- "operator": "AND",
- "conditions": [
- {
- "field": "createdAt",
- "operator": "greaterThan",
- "value": "{{now - 7 days}}"
- }
- ]
-}
-```
-
-### Inactive users
-
-Users who haven't logged in for 30+ days:
-
-```json
-{
- "operator": "AND",
- "conditions": [
- {
- "field": "data.lastLoginAt",
- "operator": "lessThan",
- "value": "{{now - 30 days}}"
- },
- {
- "field": "data.lastLoginAt",
- "operator": "exists",
- "value": true
- }
- ]
-}
-```
-
-### High-value customers
-
-Total spending over $1000:
-
-```json
-{
- "operator": "AND",
- "conditions": [
- {
- "field": "data.totalSpent",
- "operator": "greaterThanOrEquals",
- "value": 1000
- },
- {
- "field": "subscribed",
- "operator": "equals",
- "value": true
- }
- ]
-}
-```
-
-### Free tier churned users
-
-Users who downgraded from paid to free:
-
-```json
-{
- "operator": "AND",
- "conditions": [
- { "field": "data.plan", "operator": "equals", "value": "free" },
- { "field": "data.previousPlan", "operator": "in", "value": ["pro", "premium"] },
- { "field": "data.downgradedAt", "operator": "exists", "value": true }
- ]
-}
-```
-
-## Membership tracking
-
-When you enable `trackMembership`:
-
-**What happens:**
-- Plunk computes and stores segment membership
-- When contacts enter, sends `segment.entered` event
-- When contacts exit, sends `segment.exited` event
-- Provides historical membership data
-
-**Use when:**
-- You want to trigger workflows on entry/exit
-- You need to track cohort changes over time
-- Segment is relatively stable (not millions of changes per day)
-
-**Disable when:**
-- Segment changes very frequently
-- You only need current membership (not history)
-- You have millions of contacts and want to save storage
-
-### Using entry/exit events
-
-With membership tracking enabled, use segment events to trigger workflows:
-
-```json
-{
- "triggerType": "SEGMENT_ENTRY",
- "triggerConfig": {
- "segmentId": "high-value-customers"
- }
-}
-```
-
-Or use the generic event trigger:
-
-```json
-{
- "triggerType": "EVENT",
- "triggerConfig": {
- "eventName": "segment.entered"
- }
-}
-```
-
-Event data includes:
-```json
-{
- "segmentId": "seg_abc123",
- "segmentName": "High Value Customers"
-}
-```
-
-## Using segments
-
-### In campaigns
-
-Send a campaign to a segment:
-
-```bash
-curl -X POST {{API_URL}}/campaigns \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Premium Feature Announcement",
- "audienceType": "SEGMENT",
- "segmentId": "premium-users",
- "templateId": "feature-announcement"
- }'
-```
-
-### In workflows
-
-Trigger workflows when contacts enter a segment:
-
-```bash
-curl -X POST {{API_URL}}/workflows \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "VIP Welcome",
- "triggerType": "SEGMENT_ENTRY",
- "triggerConfig": {
- "segmentId": "high-value-customers"
- }
- }'
-```
-
-## Managing segments
-
-### Get segment with count
-
-```bash
-curl -X GET {{API_URL}}/segments/segment_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Returns member count:
-
-```json
-{
- "id": "segment_id",
- "name": "Premium Users",
- "memberCount": 1542,
- "filters": {...}
-}
-```
-
-### List segment members
-
-```bash
-curl -X GET "{{API_URL}}/segments/segment_id/contacts?limit=50" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### Update segment
-
-```bash
-curl -X PATCH {{API_URL}}/segments/segment_id \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Updated name",
- "filters": {
- "operator": "AND",
- "conditions": [...]
- }
- }'
-```
-
-When you update filters, membership is recomputed automatically.
-
-### Delete segment
-
-```bash
-curl -X DELETE {{API_URL}}/segments/segment_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Active campaigns and workflows using this segment will stop working.
-
-## Best practices
-
-**Start broad, then narrow** — Create general segments first, then add specific conditions.
-
-**Test your filters** — Preview segment size before using in campaigns to avoid sending to wrong audience.
-
-**Name clearly** — Use descriptive names: "Q4 2024 Premium Signups" not "Segment 3".
-
-**Avoid overlapping segments** — If using in workflows, ensure segments don't overlap to prevent duplicate emails.
-
-**Use for analysis** — Create segments to understand user cohorts, even if not used in campaigns.
-
-**Combine with events** — Track events and use them in segment conditions for behavior-based targeting.
-
-## What's next
-
-- [Build campaigns](/guides/campaigns) to send to segments
-- [Create workflows](/guides/workflows) triggered by segment entry
-- [Track events](/guides/events) to use in segment filters
diff --git a/apps/wiki/content/docs/guides/templates.mdx b/apps/wiki/content/docs/guides/templates.mdx
deleted file mode 100644
index 39aa5e5..0000000
--- a/apps/wiki/content/docs/guides/templates.mdx
+++ /dev/null
@@ -1,287 +0,0 @@
----
-title: Templates
-description: Create reusable email templates
----
-
-## Why use templates
-
-Templates let you design emails once and reuse them across:
-- Transactional API sends (`/v1/send`)
-- Automated workflows
-
-Benefits:
-- Update design in one place, applies everywhere
-- Maintain consistent branding
-- Separate content from code
-
-## Template types
-
-### Marketing templates
-
-**Use for:** Newsletters, promotions, announcements
-
-- Only sends to subscribed contacts
-- Automatically includes unsubscribe link
-- Respects subscription preferences
-
-### Transactional templates
-
-**Use for:** Order confirmations, password resets, receipts
-
-- Sends to all contacts, even if unsubscribed
-- No unsubscribe link required
-- Critical business communications
-
-Choose the right type based on content, not delivery method. You can use both types in campaigns, workflows, and API calls. The type determines subscription enforcement.
-
-## Creating templates
-
-### In the dashboard
-
-1. Go to **Templates**
-2. Click **Create Template**
-3. Choose type (Marketing or Transactional)
-4. Set subject, from address, and body
-5. Add variables using `{{variableName}}`
-6. Save
-
-### Via API
-
-```bash
-curl -X POST {{API_URL}}/templates \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "order-confirmation",
- "subject": "Order #{{orderNumber}} confirmed",
- "body": "
Thanks for your order!
Order #{{orderNumber}} will arrive by {{deliveryDate}}.
",
- "from": "orders@example.com",
- "fromName": "Acme Store",
- "type": "TRANSACTIONAL"
- }'
-```
-
-## Using variables
-
-Variables let you personalize each email. Use `{{variableName}}` syntax:
-
-```html
-
Hello {{firstName}}!
-
Your {{plan}} subscription renews on {{renewalDate}}.
-
Total: ${{amount}}
-```
-
-When sending, provide values in the `data` object:
-
-```json
-{
- "template": "subscription-renewal",
- "data": {
- "firstName": "Sarah",
- "plan": "Pro",
- "renewalDate": "April 15, 2024",
- "amount": "29.00"
- }
-}
-```
-
-## Persistent vs. Temporary Data
-
-Template variables can come from **persistent contact data** or **temporary non-persistent data**:
-
-**Persistent data** — Saved to contact profile:
-```json
-{
- "to": "user@example.com",
- "subject": "Welcome {{firstName}}!",
- "body": "
Your {{plan}} subscription is active.
",
- "data": {
- "firstName": "John", // Saved to contact
- "plan": "Pro" // Saved to contact
- }
-}
-```
-
-**Non-persistent data** — Used only for this email:
-```json
-{
- "to": "user@example.com",
- "subject": "Password Reset",
- "body": "
Reset code: {{resetCode}}
Hello {{firstName}}!
",
- "data": {
- "firstName": "John", // Saved to contact
- "resetCode": {value: "ABC123", persistent: false} // NOT saved to contact
- }
-}
-```
-
-**When to use non-persistent data:**
-- One-time verification codes or tokens
-- Temporary URLs (password reset, magic links)
-- Session-specific information
-- Data that shouldn't pollute contact profiles
-
-**In workflows:**
-Non-persistent data from events is available throughout the entire workflow execution via the execution context, allowing you to use tokens/URLs across multiple workflow steps.
-
-### Fallback values
-
-Provide defaults for missing data:
-
-```html
-
-```
-
-## Sending with templates
-
-### Transactional emails
-
-Use the `template` field with the **template ID** (not the template name):
-
-```bash
-curl -X POST {{API_URL}}/v1/send \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "to": "user@example.com",
- "template": "clx123abc456",
- "data": {
- "orderNumber": "12345",
- "deliveryDate": "March 20"
- }
- }'
-```
-
-**Finding your template ID:**
-- In the dashboard: Go to Templates → Click on your template → Copy the ID from the URL or template details
-- Via API: Use `GET /templates` to list all templates with their IDs
-
-When using a template:
-- **Subject, body, from, and reply-to** are automatically taken from the template
-- **Template variables** (e.g., `{{orderNumber}}`) are populated from the `data` field
-- You can **override** any template value by explicitly providing it in the request (see example below)
-
-### Overriding template values
-
-```bash
-curl -X POST {{API_URL}}/v1/send \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "to": "user@example.com",
- "template": "clx123abc456",
- "subject": "Custom Subject (overrides template)",
- "from": {
- "name": "Custom Sender",
- "email": "custom@example.com"
- },
- "data": {
- "orderNumber": "12345"
- }
- }'
-```
-
-### In workflows
-
-When creating a **Send Email** workflow step, select the template from the dropdown. Variables are automatically filled from contact data and workflow context.
-
-### In campaigns
-
-When creating a campaign, choose a template instead of composing inline. All campaign recipients get the same template with personalized variables.
-
-## When to use inline vs templates
-
-**Use templates when:**
-- Sending the same email repeatedly
-- Multiple workflows/campaigns use same design
-- Design may change over time
-- Want to centralize branding
-
-**Use inline content when:**
-- One-off transactional emails
-- Unique per-user content
-- Testing or prototyping
-- Content is generated dynamically
-
-Example inline send:
-
-```bash
-curl -X POST {{API_URL}}/v1/send \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "to": "user@example.com",
- "subject": "Your verification code",
- "body": "
"
- }'
-```
-
-Changes apply to all future sends using this template.
-
-### Delete template
-
-```bash
-curl -X DELETE {{API_URL}}/templates/template_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Templates used in active workflows are not deleted—you'll need to update workflows first.
-
-## Best practices
-
-**Keep templates simple** — Focus on content, avoid complex layouts that break in email clients.
-
-**Test across clients** — Email rendering varies. Preview in Gmail, Outlook, Apple Mail, and mobile devices.
-
-**Use semantic HTML** — Use `
`, `
`, `` instead of styled `
` elements.
-
-**Provide all variables** — Missing variables display as empty. Use fallbacks: `{{name ?? 'Customer'}}`.
-
-**Version your templates** — For critical transactional emails, create new templates rather than editing existing ones.
-
-## Next Steps
-
-- [Create campaigns](/guides/campaigns) with your templates
-- [Build workflows](/guides/workflows) with automated emails
-- [Track performance](/guides/analytics) of your templates
diff --git a/apps/wiki/content/docs/guides/troubleshooting.mdx b/apps/wiki/content/docs/guides/troubleshooting.mdx
deleted file mode 100644
index dc0ec14..0000000
--- a/apps/wiki/content/docs/guides/troubleshooting.mdx
+++ /dev/null
@@ -1,427 +0,0 @@
----
-title: Troubleshooting
-description: Common issues and solutions
----
-
-## Authentication issues
-
-### Invalid API key error
-
-**Error:**
-```json
-{
- "code": 401,
- "error": "Unauthorized",
- "message": "Invalid API key"
-}
-```
-
-**Solutions:**
-
-1. **Verify key format** — Secret keys start with `sk_`, public keys start with `pk_`
-2. **Check Authorization header** — Must use `Bearer` format: `Authorization: Bearer sk_your_secret_key`
-3. **Ensure key hasn't been regenerated** — If you regenerated keys in dashboard, update your application
-4. **Verify project access** — Key must belong to the project you're accessing
-
-**Test your key:**
-```bash
-curl -X GET {{API_URL}}/contacts?limit=1 \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### Wrong key type for endpoint
-
-**Error:**
-```json
-{
- "code": 401,
- "error": "Unauthorized",
- "message": "This endpoint requires a secret key (sk_*)"
-}
-```
-
-**Solution:**
-
-You're using a public key (`pk_*`) for an endpoint that requires a secret key.
-
-- **Public keys** — Only work with `/v1/track` endpoint
-- **Secret keys** — Required for all other endpoints
-
-Use your secret key from **Settings > API Keys** in the dashboard.
-
-## Email sending issues
-
-### Emails not sending
-
-**Check these common causes:**
-
-1. **Billing limit reached** - Check your billing limits in Settings → Billing. If you hit your monthly limit, increase it or wait for monthly reset.
-
-2. **Template not found**
-```json
-{
- "code": 404,
- "error": "Not Found",
- "message": "Template not found"
-}
-```
-
-Verify template ID exists and belongs to your project.
-
-3. **Contact unsubscribed**
-
-Marketing templates skip unsubscribed contacts. Check contact subscription status:
-```bash
-curl -X GET {{API_URL}}/contacts/contact_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-If `subscribed: false`, use a transactional template for critical emails.
-
-4. **Project disabled**
-
-If your project is disabled, all email sends will fail. Contact support.
-
-### Emails going to spam
-
-**Common causes:**
-
-1. **No custom domain** — Emails from default domain may have lower trust
-2. **Low engagement** — Recipients not opening/clicking emails
-3. **High bounce rate** — Too many invalid email addresses
-4. **Spam complaints** — Recipients marking as spam
-
-**Solutions:**
-
-1. **Set up custom domain** — [Add your domain](/guides/custom-domains) for better deliverability
-2. **Clean your list** — Remove bounced and inactive contacts
-3. **Improve content** — Avoid spam trigger words, include clear unsubscribe link
-4. **Warm up your domain** — Start with small volumes, gradually increase
-5. **Segment your audience** — Only send relevant content to engaged users
-
-### Tracking not working
-
-**Open tracking:**
-
-Requires:
-- `trackingEnabled: true` on project
-- Recipient email client loads images
-- HTML email (not plain text)
-
-Some email clients block tracking pixels. Open rates are estimates, not exact.
-
-**Click tracking:**
-
-Requires:
-- Tracking enabled on project (check your project settings in dashboard)
-- Links in email body (not subject)
-- HTML email format
-
-## Campaign issues
-
-### Campaign won't send
-
-**Error:**
-```json
-{
- "code": 400,
- "error": "Bad Request",
- "message": "Campaign must be in DRAFT or SCHEDULED status"
-}
-```
-
-**Solution:**
-
-Can only send campaigns with status `DRAFT`. If campaign is `SENT` or `CANCELLED`, duplicate it:
-
-```bash
-curl -X POST {{API_URL}}/campaigns/campaign_id/duplicate \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### No recipients for campaign
-
-**Cause:**
-
-Campaign targets a segment or filter with zero matching contacts.
-
-**Solutions:**
-
-1. **Check segment size**
-```bash
-curl -X GET {{API_URL}}/segments/segment_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Look at `memberCount` field.
-
-2. **Verify filters** — Test filters on segments page to see matches
-
-3. **Check subscription status** — Marketing templates only send to subscribed contacts
-
-### Scheduled campaign didn't send
-
-**Check:**
-
-1. **Verify schedule time** — Must be in future when scheduled
-2. **Campaign status** — Should be `SCHEDULED`, not `CANCELLED`
-3. **Project limits** — Billing limits may block sends
-
-View campaign details:
-```bash
-curl -X GET {{API_URL}}/campaigns/campaign_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-## Workflow issues
-
-### Workflow not triggering
-
-**Common causes:**
-
-1. **Workflow not enabled**
-
-Check `enabled: true`:
-```bash
-curl -X GET {{API_URL}}/workflows/workflow_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-2. **Wrong event name**
-
-Event names are case-sensitive. `user_signed_up` ≠ `User_Signed_Up`
-
-Verify event name exactly matches workflow trigger.
-
-3. **Contact already entered (allowReentry: false)**
-
-If `allowReentry: false`, contact can only enter once. Check execution history:
-```bash
-curl -X GET {{API_URL}}/workflows/workflow_id/executions?contactId=contact_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-4. **Segment membership not tracked**
-
-For `SEGMENT_ENTRY` triggers, segment must have `trackMembership: true`.
-
-### Workflow emails not sending
-
-**Check each email step execution:**
-
-```bash
-curl -X GET {{API_URL}}/workflows/workflow_id/executions/execution_id \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Look for step failures in response.
-
-**Common causes:**
-
-1. **Template not found** — Template was deleted
-2. **Contact unsubscribed** — Marketing templates skip unsubscribed contacts
-3. **Billing limit reached** — Monthly workflow email limit exceeded
-4. **Workflow execution stopped** — Contact was deleted or execution was cancelled
-
-### Workflow stuck on delay step
-
-Delays are processed by background jobs. Check:
-
-1. **scheduledFor** time — When should it execute?
-2. **Current time** — Has the scheduled time passed?
-
-Delays process every minute. Wait a few minutes and check again.
-
-## Contact issues
-
-### Contact not found
-
-**Error:**
-```json
-{
- "code": 404,
- "error": "Not Found",
- "message": "Contact not found"
-}
-```
-
-**Solutions:**
-
-1. **Verify contact ID** — Check for typos in the ID
-2. **Check project** — Contact may belong to different project
-3. **Contact deleted** — Contact may have been deleted
-
-Search by email instead:
-```bash
-curl -X GET "{{API_URL}}/contacts?search=user@example.com" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### Duplicate contacts
-
-Plunk automatically prevents duplicates. Creating a contact with existing email updates that contact instead of creating a new one.
-
-If you see duplicates:
-1. Check email addresses carefully (they may differ slightly)
-2. Verify you're viewing the same project
-
-### Contact data not updating
-
-**Verify data format:**
-
-```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"
- }
- }'
-```
-
-**Common issues:**
-
-1. **Missing `data` wrapper** — Custom fields must be inside `data` object
-2. **Wrong data types** — Numbers should be `99`, not `"99"`
-3. **Nested too deeply** — Keep data relatively flat
-
-## Segment issues
-
-### Segment shows wrong count
-
-Segment counts update every 5 minutes. Wait a few minutes and refresh.
-
-For real-time count, query members directly:
-```bash
-curl -X GET "{{API_URL}}/segments/segment_id/contacts?limit=1" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Check `total` field in response.
-
-### Segment filter not working
-
-**Test your filter:**
-
-1. **Check field names** — Use `data.fieldName` for custom fields
-2. **Verify operators** — `equals` for strings, `greaterThan` for numbers
-3. **Match data types** — Don't compare string `"99"` with number `99`
-
-**Example filters:**
-
-```javascript
-// ❌ Wrong
-{ "field": "plan", "operator": "equals", "value": "pro" }
-
-// ✓ Correct
-{ "field": "data.plan", "operator": "equals", "value": "pro" }
-```
-
-### Segment entry/exit events not firing
-
-Requires `trackMembership: true` on segment:
-
-```bash
-curl -X PATCH {{API_URL}}/segments/segment_id \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{"trackMembership": true}'
-```
-
-Membership is computed every 5 minutes. Events fire on the next computation cycle after a contact's segment membership changes.
-
-## Domain verification issues
-
-### Domain won't verify
-
-**Common causes:**
-
-1. **DNS not propagated** — Can take up to 48 hours
-2. **Wrong DNS records** — Double-check DKIM tokens
-3. **Conflicting records** — Remove old DKIM records from other email services
-
-**Check DNS propagation:**
-```bash
-dig TXT _domainkey.yourdomain.com
-```
-
-Records should match the DKIM tokens provided by Plunk.
-
-**Force verification check:**
-```bash
-curl -X POST {{API_URL}}/domains/domain_id/verify \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### Emails not sending from custom domain
-
-1. **Verify domain is verified** — Check `verified: true` in domain settings
-2. **Use correct `from` address** — Must be `@yourdomain.com`
-3. **Check SPF and DKIM** — Ensure DNS records are correct
-
-## Rate limiting
-
-### 429 Too Many Requests
-
-**Error:**
-```json
-{
- "code": 429,
- "error": "Too Many Requests",
- "message": "Rate limit exceeded"
-}
-```
-
-**Solutions:**
-
-1. **Implement exponential backoff** — Wait and retry with increasing delays
-2. **Batch operations** — Group multiple operations when possible
-3. **Spread requests** — Distribute load over time instead of bursts
-
-**Example retry logic:**
-```javascript
-async function sendWithRetry(data, maxRetries = 3) {
- for (let i = 0; i < maxRetries; i++) {
- try {
- const response = await fetch('{{API_URL}}/v1/send', {
- method: 'POST',
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(data)
- });
-
- if (response.status === 429) {
- const waitTime = Math.pow(2, i) * 1000; // Exponential backoff
- await new Promise(resolve => setTimeout(resolve, waitTime));
- continue;
- }
-
- return response;
- } catch (error) {
- if (i === maxRetries - 1) throw error;
- }
- }
-}
-```
-
-## Getting help
-
-If you're still experiencing issues:
-
-1. **Check API status** — Is there a known outage?
-2. **Review error message** — Error messages usually indicate the problem
-3. **Search documentation** — Look for specific error codes or messages
-4. **Check GitHub issues** — Similar issues may already be reported
-5. **Join Discord community** — Ask for help from other users
-6. **Contact support** — Provide error details, request IDs, and steps to reproduce
-
-**Include in support requests:**
-- Error message and status code
-- API endpoint and method
-- Request body (remove sensitive data)
-- Timestamp of the issue
-- Project ID (if applicable)
-
diff --git a/apps/wiki/content/docs/guides/webhooks.mdx b/apps/wiki/content/docs/guides/webhooks.mdx
deleted file mode 100644
index 9d62b35..0000000
--- a/apps/wiki/content/docs/guides/webhooks.mdx
+++ /dev/null
@@ -1,262 +0,0 @@
----
-title: Webhooks
-description: Send real-time notifications to external services
----
-
-## What are webhooks
-
-Webhooks let you send HTTP requests to external services from within workflows. Use them to:
-
-- Notify your CRM when workflows complete
-- Update external databases with contact actions
-- Trigger third-party automations
-- Sync data across systems
-- Track workflow progress in analytics tools
-
-## Using webhooks in workflows
-
-Add a **Webhook** step to any workflow:
-
-1. Go to **Workflows**
-2. Create or edit a workflow
-3. Add a **Webhook** step
-4. Configure the HTTP request
-5. Connect to other steps
-
-### Basic webhook configuration
-
-```json
-{
- "type": "WEBHOOK",
- "config": {
- "url": "https://your-api.com/webhook",
- "method": "POST",
- "headers": {
- "Content-Type": "application/json",
- "Authorization": "Bearer your_api_token"
- },
- "body": {
- "email": "{{email}}",
- "event": "workflow_completed",
- "contactId": "{{id}}"
- }
- }
-}
-```
-
-### Supported HTTP methods
-
-- **POST** — Most common, sends data to endpoint
-- **PUT** — Update existing resource
-- **PATCH** — Partial update
-- **GET** — Retrieve data (rarely used in workflows)
-- **DELETE** — Remove resource
-
-## Using contact variables
-
-Access contact data in your webhook using template variables:
-
-```json
-{
- "url": "https://crm.example.com/contacts",
- "method": "POST",
- "body": {
- "email": "{{email}}",
- "firstName": "{{data.firstName}}",
- "lastName": "{{data.lastName}}",
- "plan": "{{data.plan}}",
- "workflowName": "{{workflowName}}",
- "completedAt": "{{now}}"
- }
-}
-```
-
-Available variables:
-- `{{email}}` — Contact email
-- `{{id}}` — Contact ID
-- `{{data.fieldName}}` — Any custom data field
-- `{{workflowName}}` — Current workflow name
-- `{{now}}` — Current timestamp
-
-## Common use cases
-
-### Notify Slack
-
-```json
-{
- "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
- "method": "POST",
- "body": {
- "text": "New user completed onboarding: {{email}}"
- }
-}
-```
-
-### Update CRM
-
-```json
-{
- "url": "https://api.crm.com/contacts/{{data.crmId}}",
- "method": "PATCH",
- "headers": {
- "Authorization": "Bearer crm_api_token"
- },
- "body": {
- "onboardingCompleted": true,
- "lastEngaged": "{{now}}"
- }
-}
-```
-
-### Track analytics
-
-```json
-{
- "url": "https://analytics.example.com/events",
- "method": "POST",
- "headers": {
- "X-API-Key": "analytics_key"
- },
- "body": {
- "event": "workflow_milestone",
- "userId": "{{email}}",
- "properties": {
- "workflow": "{{workflowName}}",
- "step": "purchase_completed"
- }
- }
-}
-```
-
-### Trigger Zapier
-
-```json
-{
- "url": "https://hooks.zapier.com/hooks/catch/YOUR_WEBHOOK_ID/",
- "method": "POST",
- "body": {
- "email": "{{email}}",
- "firstName": "{{data.firstName}}",
- "event": "trial_ended"
- }
-}
-```
-
-## Error handling
-
-### Webhook failures
-
-If a webhook request fails:
-- Workflow continues to next step
-- Error is logged in workflow execution
-- Contact is not blocked
-
-Check workflow execution logs to see webhook errors:
-
-```bash
-curl -X GET {{API_URL}}/workflows/workflow_id/executions \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-### Timeouts
-
-Webhooks timeout after 30 seconds. If your endpoint takes longer:
-- Use async processing on your end
-- Return 202 Accepted immediately
-- Process in background job
-
-### Retry logic
-
-Webhooks are **not automatically retried**. If you need guaranteed delivery:
-- Implement retry logic in your endpoint
-- Use a message queue (SQS, RabbitMQ)
-- Track webhook status in your database
-
-## Receiving email event webhooks
-
-Plunk tracks email events automatically (opens, clicks, bounces). Access them via:
-
-### Events API
-
-```bash
-curl -X GET "{{API_URL}}/events?contactId=contact_id" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Returns email events:
-```json
-{
- "events": [
- {
- "name": "email.opened",
- "data": { "emailId": "...", "timestamp": "..." }
- },
- {
- "name": "email.clicked",
- "data": { "emailId": "...", "url": "...", "timestamp": "..." }
- }
- ]
-}
-```
-
-### Trigger workflows on email events
-
-Create a workflow triggered by email events:
-
-```json
-{
- "triggerType": "EVENT",
- "triggerConfig": {
- "eventName": "email.clicked"
- }
-}
-```
-
-Then use a webhook step to forward to your system.
-
-## Security best practices
-
-**Use HTTPS only** — Never send sensitive data over HTTP.
-
-**Authenticate requests** — Include API tokens in headers:
-```json
-{
- "headers": {
- "Authorization": "Bearer your_secret_token"
- }
-}
-```
-
-**Validate on receiving end** — Don't trust webhook data blindly. Verify it matches your expectations.
-
-**Don't expose secrets** — Store API tokens as environment variables, not in workflow config.
-
-**Rate limit your endpoint** — Protect against webhook floods.
-
-## Testing webhooks
-
-### Use webhook.site
-
-For testing, use [webhook.site](https://webhook.site):
-
-1. Go to webhook.site
-2. Copy your unique URL
-3. Use it in your workflow webhook step
-4. Trigger the workflow
-5. See the request in webhook.site
-
-### Test with ngrok
-
-For local development:
-
-```bash
-ngrok http 3000
-```
-
-Use the ngrok URL in your webhook configuration. Requests will tunnel to your local server.
-
-## What's next
-
-- [Build workflows](/guides/workflows) with webhook steps
-- [Track events](/guides/events) to trigger webhooks
-- [Monitor analytics](/guides/analytics) for webhook success rates
diff --git a/apps/wiki/content/docs/guides/workflow-automation.mdx b/apps/wiki/content/docs/guides/workflow-automation.mdx
deleted file mode 100644
index 1fa8911..0000000
--- a/apps/wiki/content/docs/guides/workflow-automation.mdx
+++ /dev/null
@@ -1,211 +0,0 @@
----
-title: Advanced Workflow Automation
-description: Control workflow re-entry and pass execution context data
----
-
-## Workflow Re-Entry Control
-
-Control whether contacts can enter the same workflow multiple times.
-
-### Allow Re-Entry
-
-Use when the workflow represents a repeating process:
-
-```javascript
-{
- "name": "Weekly Newsletter",
- "triggerType": "EVENT",
- "triggerConfig": { "eventName": "newsletter.send" },
- "allowReentry": true // Contacts can re-enter
-}
-```
-
-**When to use**:
-- Recurring campaigns (weekly newsletters)
-- Event-based sequences (cart abandoned)
-- Behavior triggers that can happen multiple times
-
-**What happens**: Contact can have multiple active executions of the same workflow.
-
-### Prevent Re-Entry (Default)
-
-Use for one-time journeys:
-
-```javascript
-{
- "name": "Onboarding Series",
- "triggerType": "EVENT",
- "triggerConfig": { "eventName": "user.signup" },
- "allowReentry": false // One-time only (default)
-}
-```
-
-**When to use**:
-- User onboarding
-- Trial expiration
-- Welcome sequences
-
-**What happens**: Contact can only enter once, even if the trigger fires again.
-
-## Execution Context
-
-Pass event-specific data when starting a workflow execution.
-
-### Basic Example
-
-```javascript
-// Start workflow with order-specific context
-fetch('/workflows/workflow_id/executions', {
- method: 'POST',
- body: JSON.stringify({
- contactId: 'contact_id',
- context: {
- orderNumber: 'ORD-12345',
- orderTotal: 299.99,
- deliveryDate: '2024-03-30'
- }
- })
-});
-```
-
-In your workflow email templates:
-
-```html
-
Hi {'{{firstName}}'}!
-
Order #{'{{orderNumber}}'}: ${'{{orderTotal}}'}
-
Delivery: {'{{deliveryDate}}'}
-```
-
-The template accesses both contact data (`firstName`) and context data (`orderNumber`, `orderTotal`, `deliveryDate`).
-
-### When to Use Context
-
-**Use execution context for**:
-- Order-specific details
-- Event registration info
-- Session data
-- Campaign-specific values
-
-**Update contact data for**:
-- Persistent user attributes
-- Cumulative metrics (total orders, lifetime value)
-- Segment-able fields
-
-### Example: Order Confirmation Workflow
-
-```javascript
-// Trigger workflow on purchase
-{
- "name": "Order Confirmation",
- "triggerType": "EVENT",
- "triggerConfig": { "eventName": "purchase.completed" },
- "allowReentry": true // Can purchase multiple times
-}
-
-// Start execution with order context
-fetch('/workflows/order_workflow_id/executions', {
- method: 'POST',
- body: JSON.stringify({
- contactId: 'contact_id',
- context: {
- orderNumber: 'ORD-456',
- total: 199.99,
- trackingUrl: 'https://track.example.com/456'
- }
- })
-});
-```
-
-Workflow sends emails with order-specific details while tracking cumulative purchase data on the contact.
-
-## Real-World Patterns
-
-### Pattern 1: Trial Workflow
-
-```javascript
-{
- "name": "14-Day Trial",
- "triggerType": "EVENT",
- "triggerConfig": { "eventName": "trial.started" },
- "allowReentry": false // Only trial once
-}
-
-// Workflow steps:
-// Day 0: Welcome email
-// Day 7: Mid-trial check-in
-// Day 13: Upgrade reminder
-```
-
-### Pattern 2: Cart Abandonment
-
-```javascript
-{
- "name": "Cart Abandoned",
- "triggerType": "EVENT",
- "triggerConfig": { "eventName": "cart.abandoned" },
- "allowReentry": true // Can abandon multiple times
-}
-
-// Pass cart data as context
-context: {
- cartTotal: 99.99,
- cartUrl: 'https://app.example.com/cart/abc'
-}
-```
-
-### Pattern 3: Event Reminders
-
-```javascript
-{
- "name": "Webinar Reminders",
- "triggerType": "EVENT",
- "triggerConfig": { "eventName": "webinar.registered" },
- "allowReentry": true // Can register for multiple webinars
-}
-
-// Pass webinar details as context
-context: {
- webinarTitle: 'Email Automation Masterclass',
- webinarDate: '2024-04-10',
- webinarLink: 'https://zoom.us/j/12345'
-}
-```
-
-## Monitoring Executions
-
-Check workflow execution status:
-
-```bash
-curl -X GET "/workflows/workflow_id/executions/execution_id" \
- -H "Authorization: Bearer sk_your_secret_key"
-```
-
-Response shows step progress:
-
-```json
-{
- "success": true,
- "data": {
- "id": "execution_id",
- "status": "active",
- "steps": [
- {
- "stepId": "step_1",
- "status": "completed",
- "completedAt": "2024-03-15T10:00:00Z"
- },
- {
- "stepId": "step_2",
- "status": "waiting",
- "waitingUntil": "2024-03-16T10:00:00Z"
- }
- ]
- }
-}
-```
-
-## Next Steps
-
-- [Create workflows](/guides/workflows) to automate sequences
-- [Track events](/guides/events) to trigger workflows
-- [Set up segments](/guides/segments) for segment-based triggers
diff --git a/apps/wiki/content/docs/guides/workflows.mdx b/apps/wiki/content/docs/guides/workflows.mdx
deleted file mode 100644
index 26cf940..0000000
--- a/apps/wiki/content/docs/guides/workflows.mdx
+++ /dev/null
@@ -1,445 +0,0 @@
----
-title: Workflows
-description: Automate email sequences with triggers and conditions
----
-
-## What are workflows
-
-Workflows automate email sequences based on user behavior. Build onboarding drips, re-engagement campaigns, and event-triggered emails—all without writing code.
-
-A workflow consists of:
-- **Trigger** — What starts the workflow (event, segment entry, schedule)
-- **Steps** — Actions like sending emails, waiting, or checking conditions
-- **Transitions** — Connections between steps that define the flow
-
-## Common use cases
-
-### Welcome series
-
-Send a 3-email onboarding sequence when users sign up:
-
-1. User triggers `signed_up` event
-2. Send welcome email immediately
-3. Wait 1 day
-4. Send getting started guide
-5. Wait 2 days
-6. Send feature tips
-
-### Abandoned cart recovery
-
-Re-engage users who add items but don't purchase:
-
-1. User triggers `cart_abandoned` event
-2. Wait 1 hour
-3. Send reminder email with cart contents
-4. Wait for `purchase` event (timeout: 24 hours)
-5. If purchased → Exit workflow
-6. If timeout → Send discount offer
-
-### Trial expiration
-
-Notify users before trial ends and encourage upgrade:
-
-1. Trigger daily at 9am
-2. Check if trial expires in 3 days
-3. If yes → Send upgrade reminder
-4. Wait for `subscription_created` event (timeout: 3 days)
-5. If subscribed → Send thank you email
-6. If timeout → Send last chance offer
-
-### Re-engagement campaign
-
-Win back inactive users:
-
-1. Contact enters "Inactive Users" segment
-2. Send "We miss you" email
-3. Wait for `login` event (timeout: 7 days)
-4. If logged in → Exit workflow
-5. If timeout → Send special offer
-
-## Creating workflows
-
-### In the dashboard
-
-1. Go to **Workflows**
-2. Click **Create Workflow**
-3. Name your workflow
-4. Choose trigger type
-5. Add steps using visual builder
-6. Connect steps with transitions
-7. Activate workflow
-
-### Via API
-
-```bash
-curl -X POST {{API_URL}}/workflows \
- -H "Authorization: Bearer sk_your_secret_key" \
- -H "Content-Type: application/json" \
- -d '{
- "name": "Welcome Series",
- "eventName": "signed_up",
- "enabled": true
- }'
-```
-
-Then add steps and transitions through the dashboard or API.
-
-## Trigger types
-
-### Event trigger
-
-Starts when a specific event is tracked:
-
-```json
-{
- "triggerType": "EVENT",
- "triggerConfig": {
- "eventName": "signed_up"
- }
-}
-```
-
-Track the event with `/v1/track` to start the workflow.
-
-### Segment entry
-
-Starts when contact joins a segment:
-
-```json
-{
- "triggerType": "SEGMENT_ENTRY",
- "triggerConfig": {
- "segmentId": "premium-users"
- }
-}
-```
-
-Requires segment to have `trackMembership: true`.
-
-### Segment exit
-
-Starts when contact leaves a segment:
-
-```json
-{
- "triggerType": "SEGMENT_EXIT",
- "triggerConfig": {
- "segmentId": "trial-users"
- }
-}
-```
-
-### Schedule
-
-Runs on a cron schedule:
-
-```json
-{
- "triggerType": "SCHEDULE",
- "triggerConfig": {
- "schedule": "0 9 * * *"
- }
-}
-```
-
-Evaluates all contacts—use conditions to filter who continues.
-
-## Workflow steps
-
-### Send Email
-
-Send a template to the contact:
-
-```json
-{
- "type": "SEND_EMAIL",
- "config": {
- "templateId": "welcome-template"
- }
-}
-```
-
-### Delay
-
-Wait before continuing:
-
-```json
-{
- "type": "DELAY",
- "config": {
- "duration": 86400,
- "unit": "seconds"
- }
-}
-```
-
-Common durations:
-- 1 hour: `3600`
-- 1 day: `86400`
-- 1 week: `604800`
-
-### Wait for Event
-
-Pause until an event occurs or timeout:
-
-```json
-{
- "type": "WAIT_FOR_EVENT",
- "config": {
- "eventName": "purchase",
- "timeout": 604800
- }
-}
-```
-
-Create two transitions: one for "success" (event occurred) and one for "timeout".
-
-### Condition
-
-Branch based on contact data:
-
-```json
-{
- "type": "CONDITION",
- "config": {
- "field": "data.plan",
- "operator": "equals",
- "value": "premium"
- }
-}
-```
-
-Create two transitions: "true" and "false".
-
-### Update Contact
-
-Modify contact fields:
-
-```json
-{
- "type": "UPDATE_CONTACT",
- "config": {
- "data": {
- "onboardingCompleted": true,
- "completedAt": "{{now}}"
- }
- }
-}
-```
-
-### Webhook
-
-Call an external API:
-
-```json
-{
- "type": "WEBHOOK",
- "config": {
- "url": "https://api.example.com/webhook",
- "method": "POST",
- "body": {
- "email": "{{email}}",
- "event": "workflow_completed"
- }
- }
-}
-```
-
-### Exit
-
-End the workflow:
-
-```json
-{
- "type": "EXIT"
-}
-```
-
-## Re-entry behavior
-
-Control whether contacts can enter a workflow multiple times:
-
-### Prevent re-entry (default)
-
-**allowReentry: false**
-- Contact can only enter once, ever
-- Subsequent triggers are ignored
-- Use for one-time journeys
-
-**Best for:**
-- User onboarding sequences
-- Welcome series
-- Trial expiration flows
-- One-time educational content
-
-**Example:**
-```json
-{
- "name": "Onboarding Series",
- "eventName": "user_signed_up",
- "allowReentry": false,
- "enabled": true
-}
-```
-
-Even if the `user_signed_up` event fires multiple times for the same contact, they'll only enter once.
-
-### Allow re-entry
-
-**allowReentry: true**
-- Contact can enter multiple times
-- Each trigger starts a new execution
-- Multiple executions can run simultaneously
-
-**Best for:**
-- Recurring events (weekly newsletters, monthly reports)
-- Behavior-triggered campaigns (cart abandonment, content engagement)
-- Event-specific sequences (order confirmations, webinar reminders)
-
-**Example:**
-```json
-{
- "name": "Cart Abandoned Reminder",
- "eventName": "cart_abandoned",
- "allowReentry": true,
- "enabled": true
-}
-```
-
-User abandons cart multiple times → Each triggers a new workflow execution.
-
-## Execution context
-
-Pass event-specific data when workflows are triggered automatically.
-
-### What is context?
-
-Context is temporary data passed with a workflow execution that's available in templates but not saved to the contact record.
-
-**Contact data vs Context:**
-- **Contact data** — Persistent, saved to contact, used for segmentation
-- **Execution context** — Temporary, specific to this workflow run, not saved
-
-### Using context in templates
-
-Context data is available in workflow email templates alongside contact data:
-
-```html
-
',
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
-
- You're receiving this because you subscribed to updates.
- Unsubscribe
-
-```
-
-### 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
-
Hi {{firstName ?? 'there'}},
-
-```
-
-## 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
diff --git a/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx b/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx
index 5afdd5c..6f793a4 100644
--- a/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx
+++ b/apps/wiki/content/docs/tutorials/segment-based-targeting.mdx
@@ -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
diff --git a/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx b/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx
index b550070..8c63103 100644
--- a/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx
+++ b/apps/wiki/content/docs/tutorials/welcome-series-workflow.mdx
@@ -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_`).