docs: add new recipe pages for waitlist and sync unsubscribes
This commit is contained in:
@@ -6,3 +6,16 @@
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/*
|
||||
* Two-tone palette: white content, gray chrome.
|
||||
* `--color-fd-background` paints the page (content area + nav).
|
||||
* `--color-fd-card` paints the sidebar (via `bg-fd-card` on `#nd-sidebar`)
|
||||
* and the `<Cards>` component — both read well as soft gray against white.
|
||||
*/
|
||||
:root {
|
||||
--color-fd-background: hsl(0, 0%, 100%);
|
||||
--color-fd-card: hsl(0, 0%, 96.5%);
|
||||
--color-fd-secondary: hsl(0, 0%, 95%);
|
||||
--color-fd-border: hsla(0, 0%, 80%, 60%);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"---Docs---",
|
||||
"concepts",
|
||||
"guides",
|
||||
"---Recipes---",
|
||||
"recipes",
|
||||
"---API Reference---",
|
||||
"api-reference",
|
||||
"---Self-Hosting---",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: Double opt-in
|
||||
description: Require a confirmation click before a new signup starts receiving marketing email
|
||||
icon: MailCheck
|
||||
---
|
||||
|
||||
Double opt-in adds a confirmation step between "user signs up" and "user starts getting marketing email." It's the standard way to avoid mailing typoed addresses, role accounts, and anyone who didn't actually consent.
|
||||
|
||||
The trick is `{{subscribeUrl}}`: a per-contact link Plunk auto-injects into every send. Clicking it flips `subscribed` to `true` and fires a `contact.subscribed` event.
|
||||
|
||||
## Setup
|
||||
|
||||
import {Step, Steps} from 'fumadocs-ui/components/steps';
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
|
||||
### Create two templates
|
||||
|
||||
- A **Transactional** template for the confirmation email, containing `{{subscribeUrl}}`:
|
||||
|
||||
```html
|
||||
<p>Hi {{firstName}}, please confirm your email to start receiving updates:</p>
|
||||
<p><a href="{{subscribeUrl}}">Confirm my email</a></p>
|
||||
```
|
||||
|
||||
- A **Marketing** template for the welcome email that goes out *after* they confirm.
|
||||
|
||||
<Callout title="The confirmation must be transactional" type="warn">
|
||||
A marketing template targeted at an unsubscribed contact is [silently skipped](/concepts/contacts#emails-by-subscription-state). Use a transactional template for the confirmation specifically — it bypasses the subscription check.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Trigger the signup from your backend
|
||||
|
||||
Two calls with your secret key (`sk_*`): create the contact unsubscribed, then track the event that fires the confirmation workflow.
|
||||
|
||||
```bash
|
||||
curl https://next-api.useplunk.com/contacts \
|
||||
-H "Authorization: Bearer sk_your_secret_key" \
|
||||
-d '{ "email": "[email protected]", "subscribed": false, "data": { "firstName": "Ada" } }'
|
||||
|
||||
curl https://next-api.useplunk.com/v1/track \
|
||||
-H "Authorization: Bearer sk_your_secret_key" \
|
||||
-d '{ "event": "signup.pending", "email": "[email protected]", "subscribed": false }'
|
||||
```
|
||||
|
||||
Both calls pass `subscribed: false`. If you skip the first call and rely on `/v1/track` alone, tracking on an unknown email creates the contact — but defaults it to subscribed, which defeats the point.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Workflow A: send the confirmation
|
||||
|
||||
**Workflows → New workflow**:
|
||||
|
||||
- **Trigger**: `EVENT` on `signup.pending`
|
||||
- `SEND_EMAIL` step → transactional confirmation template
|
||||
|
||||
Enable it.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Workflow B: welcome them after confirmation
|
||||
|
||||
**Workflows → New workflow**:
|
||||
|
||||
- **Trigger**: `EVENT` on `contact.subscribed`
|
||||
- `SEND_EMAIL` step → marketing welcome template
|
||||
|
||||
Enable it. `contact.subscribed` fires whenever a contact opts in — including via `{{subscribeUrl}}`, the preferences page, or the API — so this workflow handles both first-time confirmations and resubscribes.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Reminder if they don't confirm
|
||||
|
||||
Extend Workflow A with a `WAIT_FOR_EVENT` step after the send:
|
||||
|
||||
- **Event**: `contact.subscribed`
|
||||
- **Timeout**: `86400` (24 hours)
|
||||
|
||||
On timeout, send a single reminder (also transactional). Keep the number of reminders small — repeated confirmation prompts look like spam to mailbox providers as much as to recipients.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Unsubscribe & preferences pages" href="/guides/unsubscribe-pages">
|
||||
Detail on `{{subscribeUrl}}` and the hosted pages.
|
||||
</Card>
|
||||
<Card title="Templates" href="/concepts/templates">
|
||||
The difference between Marketing, Transactional, and Headless templates.
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Recipes
|
||||
description: End-to-end walkthroughs for common patterns built on Plunk events and workflows
|
||||
icon: ChefHat
|
||||
---
|
||||
|
||||
Recipes are concrete, step-by-step builds for patterns we see most often in Plunk projects. Each one assumes you already understand the underlying [concepts](/concepts/workflows) and walks you through the exact API calls, workflow steps, and template variables involved.
|
||||
|
||||
<Cards>
|
||||
<Card title="Waitlist with confirmation email" href="/recipes/waitlist">
|
||||
Capture signups with a single tracked event, then automatically email each person who joins.
|
||||
</Card>
|
||||
<Card title="Sync unsubscribes to your database" href="/recipes/sync-unsubscribes">
|
||||
Keep your own user table in step with Plunk's subscription state using a webhook step.
|
||||
</Card>
|
||||
<Card title="Double opt-in" href="/recipes/double-opt-in">
|
||||
Add a confirmation step before a contact starts receiving marketing email, using `{{subscribeUrl}}`.
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["index", "waitlist", "sync-unsubscribes", "double-opt-in"]
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: Sync unsubscribes to your database
|
||||
description: Mirror Plunk's subscription state into your own user table using a workflow + webhook
|
||||
icon: RefreshCw
|
||||
---
|
||||
|
||||
Every flip of a contact's `subscribed` state — manual edits, the hosted unsubscribe page, bounces, complaints — fires a `contact.unsubscribed` event. Wire a workflow with a `WEBHOOK` step to forward that to your backend.
|
||||
|
||||
## Setup
|
||||
|
||||
import {Step, Steps} from 'fumadocs-ui/components/steps';
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
|
||||
### Build the receiving endpoint
|
||||
|
||||
A public HTTPS endpoint that verifies a shared secret and updates the user row. Webhook requests time out after 10 seconds, so do the work async if it's slow.
|
||||
|
||||
```ts
|
||||
app.post('/plunk/unsubscribes', async (req, res) => {
|
||||
if (req.header('authorization') !== `Bearer ${process.env.PLUNK_WEBHOOK_SECRET}`) {
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
const { contact, event } = req.body;
|
||||
await db.user.update({
|
||||
where: { email: contact.email },
|
||||
data: {
|
||||
emailSubscribed: false,
|
||||
emailUnsubscribedReason: event.reason ?? 'user_action',
|
||||
},
|
||||
});
|
||||
|
||||
res.status(204).end();
|
||||
});
|
||||
```
|
||||
|
||||
`event.reason` is `"bounce"` or `"complaint"` for automatic unsubscribes, and absent for manual / self-service ones.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Create the workflow
|
||||
|
||||
**Workflows → New workflow**:
|
||||
|
||||
- **Trigger**: `EVENT` on `contact.unsubscribed`
|
||||
- Add a `WEBHOOK` step:
|
||||
- **URL**: `https://api.example.com/plunk/unsubscribes`
|
||||
- **Headers**: `{ "Authorization": "Bearer your-shared-secret" }`
|
||||
- Leave the body blank to get the [default payload](/guides/webhooks#webhook-payload).
|
||||
|
||||
Enable the workflow.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Mirroring resubscribes
|
||||
|
||||
Build a second workflow with the same shape, triggered by `contact.subscribed`. Keep it separate from the unsubscribe flow — two short workflows are easier to monitor than one branched one.
|
||||
|
||||
## The reverse direction
|
||||
|
||||
If your product is the source of truth (a user toggles their email preference in your settings UI), call `PATCH /contacts/:id` from your backend:
|
||||
|
||||
```bash
|
||||
curl -X PATCH https://next-api.useplunk.com/contacts/cnt_abc \
|
||||
-H "Authorization: Bearer sk_your_secret_key" \
|
||||
-d '{"subscribed": false}'
|
||||
```
|
||||
|
||||
That flip also fires `contact.unsubscribed`, meaning your own webhook will round-trip back into your handler. That's usually harmless because the update is idempotent — but be aware of it.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Webhooks" href="/guides/webhooks">
|
||||
Webhook step reference, payload shape, and safety.
|
||||
</Card>
|
||||
<Card title="Unsubscribe pages" href="/guides/unsubscribe-pages">
|
||||
The hosted pages and template URL variables.
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
title: Waitlist with confirmation email
|
||||
description: Track signups as a custom event, store everyone who joins as a contact, and automatically email them
|
||||
icon: ListOrdered
|
||||
---
|
||||
|
||||
A waitlist is the simplest possible Plunk workflow: one tracked event from your app, one workflow that listens for it, one email.
|
||||
|
||||
## Setup
|
||||
|
||||
import {Step, Steps} from 'fumadocs-ui/components/steps';
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
|
||||
### Create the confirmation template
|
||||
|
||||
In **Templates → New template**, create a **Marketing** template. Use `{{variable}}` placeholders for anything you want to personalise from contact data:
|
||||
|
||||
```text
|
||||
Subject: You're on the list, {{firstName}}
|
||||
|
||||
Hi {{firstName}}, thanks for joining the {{product}} waitlist.
|
||||
We'll let you know as soon as your spot opens up.
|
||||
```
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Track the signup from your backend
|
||||
|
||||
Call `POST /v1/track` when a user submits the form. Use a secret key (`sk_*`) — never call this from the browser.
|
||||
|
||||
```bash
|
||||
curl https://next-api.useplunk.com/v1/track \
|
||||
-H "Authorization: Bearer sk_your_secret_key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"event": "waitlist.joined",
|
||||
"email": "[email protected]",
|
||||
"data": { "firstName": "Ada", "product": "Beta" }
|
||||
}'
|
||||
```
|
||||
|
||||
This call upserts the contact (subscribed by default) and records `waitlist.joined` on them. Anything you put in `data` lands on the contact and is available as `{{firstName}}`, `{{product}}`, etc. in the template.
|
||||
|
||||
<Callout title="Pick a stable event name" type="info">
|
||||
A workflow's trigger event **cannot be changed after the first execution**. Namespace it (`waitlist.joined`) rather than something generic you might want to reuse.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Create the workflow
|
||||
|
||||
**Workflows → New workflow**:
|
||||
|
||||
- **Trigger**: `EVENT` on `waitlist.joined`
|
||||
- Add a `SEND_EMAIL` step pointing at the template from step 1
|
||||
|
||||
Enable the workflow. Workflows are created disabled — until the toggle is on, nothing fires.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Tagging signups for later
|
||||
|
||||
If you want to segment on waitlist signups later, add an `UPDATE_CONTACT` step before the email:
|
||||
|
||||
```json
|
||||
{ "stage": "waitlist", "waitlistSource": "{{event.referrer}}" }
|
||||
```
|
||||
|
||||
You can then build a [segment](/concepts/segments) of contacts where `stage == "waitlist"` to target with follow-up campaigns. This is cleaner than filtering on "ever fired `waitlist.joined`."
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Workflows" href="/concepts/workflows">
|
||||
Step types and trigger semantics.
|
||||
</Card>
|
||||
<Card title="Track event API" href="/api-reference/public-api/trackEvent">
|
||||
Full reference for `POST /v1/track`.
|
||||
</Card>
|
||||
</Cards>
|
||||
Reference in New Issue
Block a user