docs: expand documentation with new sections on importing contacts, unsubscribe pages, and API key management
This commit is contained in:
@@ -1,19 +1,89 @@
|
||||
---
|
||||
title: Billing
|
||||
description: Understand Plunk's billing model
|
||||
description: How Plunk's pricing, limits, and consumption work
|
||||
icon: CreditCard
|
||||
---
|
||||
|
||||
Plunk's pricing is based on the number of emails sent each month. You can send both marketing and transactional emails under the same plan.
|
||||
Plunk's pricing is based on the number of emails sent each month. Marketing emails, transactional emails, workflow sends, and inbound emails are all counted under the same monthly usage. Pricing on this page applies to the hosted version at [useplunk.com](https://www.useplunk.com); self-hosted instances run on their own infrastructure.
|
||||
|
||||
## Free Tier
|
||||
Free tier projects can send up to 1,000 emails per month at no cost. Projects on this tier will include a Plunk-branded footer in all emails.
|
||||
## Plans
|
||||
|
||||
## Pay-as-you-go
|
||||
After upgrading from the free tier, you will be charged per email sent at $0.001 per email. There are no monthly fees or commitments, you only pay for what you use.
|
||||
### Free tier
|
||||
|
||||
You are able to monitor your email usage and set billing limits per category in the billing tab of the project settings.
|
||||
Free tier projects can send up to 1,000 emails per month at no cost. Free-tier projects include a Plunk-branded footer in marketing emails. Inbound emails count toward the 1,000-email allowance.
|
||||
|
||||
## Special considerations
|
||||
- Emails that contain an attachment will incur double the cost (e.g. 1 email with attachment = 2 emails for billing purposes)
|
||||
- Inbound emails count towards the email limit at the same rate as outbound emails (e.g. 1 inbound email = 1 email for billing purposes)
|
||||
### Pay-as-you-go
|
||||
|
||||
After upgrading from the free tier, you're charged per email sent at $0.001 per email. There are no monthly fees and no commitments — you only pay for what you use.
|
||||
|
||||
You can monitor consumption and set per-category caps under **Settings → Billing**.
|
||||
|
||||
## What counts as an email
|
||||
|
||||
Every send and inbound receive contributes to your monthly usage:
|
||||
|
||||
| Source | Cost |
|
||||
| ------------------------------- | ------------------------------------ |
|
||||
| Transactional sends (`/v1/send`) | 1 credit per recipient |
|
||||
| Campaigns | 1 credit per recipient |
|
||||
| Workflow `SEND_EMAIL` steps | 1 credit per send |
|
||||
| Inbound emails | 1 credit per received email |
|
||||
| **Emails with attachments** | **2 credits** per email |
|
||||
|
||||
Emails with one or more attachments cost double. There's no extra charge based on attachment size — one credit per attached email regardless of whether it's 100 KB or 10 MB.
|
||||
|
||||
## Per-category billing limits
|
||||
|
||||
Set monthly caps per category to control runaway sends from any single source. This is useful if, for example, you want to ensure transactional capacity is always available even if a campaign accidentally over-sends.
|
||||
|
||||
| Category | What it caps |
|
||||
| --------------- | ------------------------------------------------------------- |
|
||||
| Transactional | Emails sent through `/v1/send` |
|
||||
| Campaigns | Emails sent as part of a campaign |
|
||||
| Workflows | Emails sent by `SEND_EMAIL` steps inside a workflow |
|
||||
| Inbound | Inbound emails received at your verified domain |
|
||||
|
||||
Configure caps in **Settings → Billing → Limits**. Each category cap is independent — you can set just one, all of them, or none.
|
||||
|
||||
## What happens when a limit is hit
|
||||
|
||||
When you exceed a per-category cap or your overall plan allowance:
|
||||
|
||||
- **Outbound API calls** (`/v1/send`, campaign sends, workflow sends) return `402 Payment Required` with error code `BILLING_LIMIT_EXCEEDED`. Your application should handle this gracefully — typically by queuing the work or surfacing an error to the user.
|
||||
- **Inbound emails** are dropped silently for the affected project until the cap resets. They aren't queued or replayed.
|
||||
- Limits reset at the start of each billing period.
|
||||
|
||||
You'll also receive a notification (email and/or in-app) when a cap is approached or hit.
|
||||
|
||||
## Invoices and payment
|
||||
|
||||
Stripe handles billing. From the **Settings → Billing** page you can:
|
||||
|
||||
- Open the **Stripe billing portal** to update payment methods, addresses, and tax IDs.
|
||||
- View and download every invoice for the project.
|
||||
- See current period consumption with a breakdown by category.
|
||||
|
||||
If a payment fails, Stripe retries on its standard schedule. Repeated failures eventually pause the project's ability to send — keep your payment method up to date to avoid disruption.
|
||||
|
||||
## API reference
|
||||
|
||||
- `GET /users/@me/projects/:id/billing-consumption` — current period usage by category.
|
||||
- `GET /users/@me/projects/:id/billing-limits` — read per-category caps.
|
||||
- `PUT /users/@me/projects/:id/billing-limits` — update per-category caps.
|
||||
- `GET /users/@me/projects/:id/billing-invoices` — list invoices.
|
||||
- `POST /users/@me/projects/:id/billing-portal` — generate a Stripe billing portal URL.
|
||||
- `POST /users/@me/projects/:id/checkout` — start a Stripe Checkout session for upgrades.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="List hygiene" href="/guides/list-hygiene">
|
||||
Reduce wasted sends by keeping bounce and complaint rates low.
|
||||
</Card>
|
||||
<Card title="Receiving emails" href="/guides/receiving-emails">
|
||||
Inbound emails count toward your monthly usage.
|
||||
</Card>
|
||||
<Card title="Self-hosting" href="/self-hosting/introduction">
|
||||
Run Plunk on your own infrastructure for full control over costs.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -1,15 +1,93 @@
|
||||
---
|
||||
title: Campaigns
|
||||
description: Broadcast to your contacts
|
||||
description: One-off broadcast emails sent to a defined audience
|
||||
icon: Megaphone
|
||||
---
|
||||
|
||||
Campaigns are a one-time email sent to a group of contacts. They are typically used for newsletters, announcements, or promotions.
|
||||
Campaigns are one-off email sends to a defined audience — newsletters, announcements, product launches, promotions. Unlike workflows, a campaign sends once at a single point in time (immediately or at a scheduled time) and is then frozen.
|
||||
|
||||
## Targeting contacts
|
||||
When you create a campaign, you can send it to all subscribed contacts, or target a specific segment you have created in the [Segments](/concepts/segments) section.
|
||||
## Anatomy of a campaign
|
||||
|
||||
## Sending a campaign
|
||||
Once you have designed your campaign and selected the target audience, you can schedule it to be sent immediately or at a later time. You can also choose to send a test email to yourself or any other team member before sending it to your contacts.
|
||||
A campaign captures everything needed to render and deliver one batch of emails:
|
||||
|
||||
Campaigns will be queued and sent in the background. You can monitor the sending progress and view detailed analytics on opens, clicks, and bounces in the campaign detail page. Depending on the size of your audience, it may take some time for all emails to be sent.
|
||||
import {TypeTable} from 'fumadocs-ui/components/type-table';
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
name: { type: 'string', required: true, description: 'Internal name shown in the dashboard.' },
|
||||
description: { type: 'string', description: 'Optional internal note.' },
|
||||
subject: { type: 'string', required: true, description: 'Email subject line. Supports the same template variables as [Templates](/concepts/templates).' },
|
||||
body: { type: 'string', required: true, description: 'HTML body. Supports template variables.' },
|
||||
from: { type: 'string', required: true, description: 'Sender email — must be on a verified domain.' },
|
||||
fromName: { type: 'string', description: 'Optional sender display name (e.g. `Acme Marketing <[email protected]>`).' },
|
||||
replyTo: { type: 'string', description: 'Optional reply-to address.' },
|
||||
type: { type: 'string', required: true, description: 'One of `MARKETING`, `TRANSACTIONAL`, or `HEADLESS`. Same semantics as [template types](/concepts/templates#templates-types). Controls the unsubscribe footer and whether unsubscribed contacts are skipped.' },
|
||||
audienceType: { type: 'string', required: true, description: 'Who this campaign is sent to. One of `ALL`, `SEGMENT`, or `FILTERED` — see below.' },
|
||||
segmentId: { type: 'string', description: 'Required when `audienceType = SEGMENT`.' },
|
||||
audienceCondition: { type: 'object', description: 'Inline filter when `audienceType = FILTERED`. Uses the same format as [segment filters](/guides/segment-filters).' },
|
||||
scheduledFor: { type: 'string', description: 'ISO 8601 timestamp. If set, the campaign sends at that time; if null, it sends immediately when you trigger send.' },
|
||||
}}
|
||||
/>
|
||||
|
||||
The campaign also keeps denormalised stats once it sends: `totalRecipients`, `sentCount`, `deliveredCount`, `openedCount`, `clickedCount`, `bouncedCount`. Fetch them via `GET /campaigns/:id/stats`.
|
||||
|
||||
## Targeting an audience
|
||||
|
||||
Campaigns support three audience types:
|
||||
|
||||
- **`ALL`** — every subscribed contact in the project. Quick way to broadcast to your full list.
|
||||
- **`SEGMENT`** — every contact that's currently a member of a specific [segment](/concepts/segments). Reuses logic you've already built.
|
||||
- **`FILTERED`** — an inline filter condition (same JSON shape as segment filters). Lets you hand-craft an audience for a one-off send without saving a reusable segment.
|
||||
|
||||
`MARKETING` and `HEADLESS` campaigns automatically skip unsubscribed contacts. `TRANSACTIONAL` campaigns deliver to everyone matching the audience regardless of subscription state — use it sparingly and only for genuine transactional content.
|
||||
|
||||
## Sender domain verification
|
||||
|
||||
The `from` address must be on a domain you've verified for sending. Creating or updating a campaign with an unverified `from` returns `403`. See [Verifying domains](/guides/verifying-domains).
|
||||
|
||||
## Lifecycle
|
||||
|
||||
| Status | Meaning |
|
||||
| ----------- | ----------------------------------------------------------------------------- |
|
||||
| `DRAFT` | Created but not yet scheduled or sent. Editable. |
|
||||
| `SCHEDULED` | Scheduled for a future time via `scheduledFor`. Cancellable. |
|
||||
| `SENDING` | Send is in progress — the queue is processing recipients. |
|
||||
| `SENT` | Send is complete. Stats are final. |
|
||||
| `CANCELLED` | Cancelled before completion. Terminal state from `DRAFT`, `SCHEDULED`, or `SENDING`. |
|
||||
|
||||
Once a campaign is `SENT` or `CANCELLED` it can't be edited or resent — duplicate it instead with `POST /campaigns/:id/duplicate` to create an editable copy.
|
||||
|
||||
## Sending and scheduling
|
||||
|
||||
Trigger a send with `POST /campaigns/:id/send`. The campaign is queued and sent in the background; depending on the audience size, this can take seconds for small lists to longer for very large ones. You can monitor progress on the campaign detail page in the dashboard.
|
||||
|
||||
To cancel a `SCHEDULED` or `SENDING` campaign, call `POST /campaigns/:id/cancel`. Cancellation stops further sending but cannot recall emails that have already been handed off to the recipient's mail server.
|
||||
|
||||
## Test sends
|
||||
|
||||
Send a single test email with `POST /campaigns/:id/test`. Body: `{ email: "[email protected]" }` — a single address, not an array. Test sends use the same template variable resolution as the real send but always send to the address you specify, so they're useful for previewing personalisation against your own contact data.
|
||||
|
||||
## Duplicating
|
||||
|
||||
`POST /campaigns/:id/duplicate` creates a new `DRAFT` campaign with the same content, audience, and settings as the source. Useful for A/B variants and recurring sends.
|
||||
|
||||
## API reference
|
||||
|
||||
See the [API overview](/api-reference/overview#campaigns) for the full set of campaign endpoints — list, get, create, update, delete, send, cancel, duplicate, test, and stats.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Templates" href="/concepts/templates">
|
||||
Create reusable email designs to send through campaigns.
|
||||
</Card>
|
||||
<Card title="Segments" href="/concepts/segments">
|
||||
Build audiences for `audienceType: SEGMENT` campaigns.
|
||||
</Card>
|
||||
<Card title="Verifying domains" href="/guides/verifying-domains">
|
||||
Required before you can send from your own domain.
|
||||
</Card>
|
||||
<Card title="List hygiene" href="/guides/list-hygiene">
|
||||
Keep bounce and complaint rates healthy.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -10,10 +10,12 @@ Contacts in Plunk represent an individual email recipient. Each contact has an i
|
||||
|
||||
Contacts can be added to your Plunk project in several ways:
|
||||
|
||||
- Using [/v1/track](/api-reference/public-api/trackEvent), when tracking an event for a contact that does not yet exist, Plunk will automatically create it.
|
||||
- Using [/contacts](/api-reference/contacts/createContact), to create a single contact.
|
||||
- Import through CSV
|
||||
- Manually through the dashboard
|
||||
- Using [`/v1/track`](/api-reference/public-api/trackEvent) — tracking an event for a contact that doesn't exist yet automatically creates it (subscribed by default).
|
||||
- Using [`POST /contacts`](/api-reference/contacts/createContact) — create or upsert a single contact. Existing emails are **updated**, not rejected; the response includes a `_meta: { isNew, isUpdate }` block and uses status `201` for new contacts and `200` for updates.
|
||||
- Bulk **CSV import** via `POST /contacts/import` — upload a CSV (≤ 5 MB) and poll `GET /contacts/import/:jobId` for status. The first column must be `email`; any other columns map to keys under `data.*`.
|
||||
- Bulk **subscribe / unsubscribe / delete** via `POST /contacts/bulk-subscribe`, `bulk-unsubscribe`, and `bulk-delete` — each accepts up to 1,000 contact IDs and returns a job ID; poll `GET /contacts/bulk/:jobId` for progress.
|
||||
- Adding emails to a **static segment** with `POST /segments/:id/members` and `createMissing: true` will create any contacts that don't already exist.
|
||||
- Manually through the dashboard.
|
||||
|
||||
## Contact Data
|
||||
|
||||
@@ -21,18 +23,37 @@ You can associate custom data with each contact using key-value pairs. This data
|
||||
|
||||
### Data types
|
||||
|
||||
Contact data types are inferred based on the value provided:
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| String | Any text value |
|
||||
| Number | Numeric values, including integers and floats |
|
||||
| Boolean | True or false values |
|
||||
| Date | Date values in ISO 8601 format |
|
||||
Contact data types are inferred from the **first non-null value** Plunk sees for a given key in your project:
|
||||
|
||||
<Callout title="Default data type" variant="idea">
|
||||
If you accidentally mix data types for a specific key, Plunk will default to treating the value as a string.
|
||||
| Type | Description |
|
||||
| ------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| String | Any text value that doesn't look like a date. |
|
||||
| Number | Numeric values, including integers and floats. |
|
||||
| Boolean | `true` or `false`. |
|
||||
| Date | A string matching `YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS[.sss]Z`. The full ISO 8601 form is required for date-aware operators in segments. |
|
||||
|
||||
The type is sampled per-project on first write — once a key is typed as `number`, subsequent string values for that key are still stored, but the **inferred type** stays `number` for the segment filter UI. If you accidentally mix types, the safest reset is to delete the field via `DELETE /contacts/fields/:field` and re-import with the type you want.
|
||||
|
||||
<Callout title="Default data type" type="info">
|
||||
If a value doesn't match number, boolean, or the ISO 8601 date format, it's typed as a string.
|
||||
</Callout>
|
||||
|
||||
### Non-persistent values
|
||||
|
||||
Send a value as `{ value, persistent: false }` to use it once for template rendering without storing it on the contact:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "[email protected]",
|
||||
"data": {
|
||||
"firstName": "Ada",
|
||||
"resetCode": { "value": "ABC123", "persistent": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`firstName` is saved on the contact; `resetCode` is available to the template for this send only and is then discarded. Use this for one-shot values like password reset codes, magic links, and one-time tokens.
|
||||
|
||||
### Special value handling
|
||||
|
||||
When creating or updating contacts, certain values are handled specially:
|
||||
@@ -43,20 +64,27 @@ When creating or updating contacts, certain values are handled specially:
|
||||
| `null` | Delete - field is removed from contact data | `{ name: null }` → Field is deleted |
|
||||
| Other values | Stored/updated normally | `{ name: "John" }` → Stored as "John" |
|
||||
|
||||
<Callout title="Removing contact data" variant="idea">
|
||||
<Callout title="Removing contact data" type="info">
|
||||
To remove a field from a contact's data, set it to `null` when creating or updating the contact. Empty strings are
|
||||
automatically filtered out and won't overwrite existing data.
|
||||
</Callout>
|
||||
|
||||
### Reserved keys
|
||||
|
||||
Certain keys are reserved by the system and automatically set by Plunk:
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| email | The contact's email address |
|
||||
| createdAt | Timestamp of when the contact was created |
|
||||
| updatedAt | Timestamp of the last update to the contact |
|
||||
| subscribed | Boolean indicating if the contact is globally subscribed or not |
|
||||
Some keys are reserved by Plunk and managed at the contact level (not inside `data`). You can read them but you can't set them through `data`:
|
||||
|
||||
| Key | Description |
|
||||
| ------------ | ------------------------------------------------------------------------ |
|
||||
| `email` | The contact's email address |
|
||||
| `createdAt` | Timestamp of when the contact was created |
|
||||
| `updatedAt` | Timestamp of the last update |
|
||||
| `subscribed` | Boolean indicating whether the contact is subscribed to marketing emails |
|
||||
|
||||
The following keys are also **silently filtered out** of any `data` payload — sending them through `/v1/track`, `POST /contacts`, or `PATCH /contacts/:id` will not store them and will not return an error:
|
||||
|
||||
`id`, `plunk_id`, `plunk_email`, `unsubscribeUrl`, `subscribeUrl`, `manageUrl`
|
||||
|
||||
The last three are auto-generated per-recipient at send time and exposed as template variables (see [Templates](/concepts/templates)).
|
||||
|
||||
### Special keys
|
||||
|
||||
@@ -66,15 +94,25 @@ Certain keys are reserved by the system and automatically set by Plunk:
|
||||
|
||||
## Subscription State
|
||||
|
||||
Every contact has a `subscribed` field that determines which types of emails they will receive. A newly created contact is subscribed by default.
|
||||
Every contact has a `subscribed` field that determines which types of emails they will receive. A newly created contact is subscribed by default when created via `/v1/track`; contacts created via `POST /contacts` use whatever value you pass (default: `false`).
|
||||
|
||||
When you update a contact, **omitting `subscribed` keeps the current state** — it is not the same as passing `false`. To change the state, pass an explicit `true` or `false`.
|
||||
|
||||
Every flip of `subscribed` automatically tracks an event on the contact:
|
||||
|
||||
- `subscribed` flipped to `true` → `contact.subscribed` event
|
||||
- `subscribed` flipped to `false` → `contact.unsubscribed` event
|
||||
|
||||
These events fire on every path that changes subscription state — manual edits, the public unsubscribe page, bulk operations, automatic unsubscribes from bounces / complaints. You can branch on them in workflows.
|
||||
|
||||
### How contacts become unsubscribed
|
||||
|
||||
A contact can become unsubscribed in several ways:
|
||||
|
||||
- **Manually** through the dashboard or via the API
|
||||
- **Self-service** by clicking the unsubscribe link in an email
|
||||
- **Automatically** when an email to the contact bounces or results in a complaint
|
||||
- **Manually** through the dashboard or via the API.
|
||||
- **Self-service** by clicking the unsubscribe link in an email (powered by the public unsubscribe pages).
|
||||
- **Automatically** on a permanent (hard) email bounce. Soft bounces don't change subscription state.
|
||||
- **Automatically** when a recipient marks one of your emails as spam (their mailbox provider reports the complaint back to Plunk).
|
||||
|
||||
### Emails by subscription state
|
||||
|
||||
@@ -90,7 +128,24 @@ The subscription state controls whether a contact receives marketing emails. Tra
|
||||
| **Automations** (headless template) | Delivered | Not delivered |
|
||||
| **Automations** (transactional template) | Delivered | Delivered |
|
||||
|
||||
<Callout title="Transactional emails and marketing templates" variant="warn">
|
||||
<Callout title="Transactional emails and marketing templates" type="warn">
|
||||
Even when using the transactional API endpoint (`/v1/send`), you cannot send a marketing template to an unsubscribed
|
||||
contact. Use a transactional template instead if the email must reach unsubscribed contacts.
|
||||
</Callout>
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Custom fields" href="/guides/custom-fields">
|
||||
How to set, type, and clean up arbitrary contact data.
|
||||
</Card>
|
||||
<Card title="Importing from CSV" href="/guides/importing-contacts">
|
||||
Bulk-load contacts and their custom fields.
|
||||
</Card>
|
||||
<Card title="Segments" href="/concepts/segments">
|
||||
Group contacts dynamically or statically for targeting.
|
||||
</Card>
|
||||
<Card title="Unsubscribe pages" href="/guides/unsubscribe-pages">
|
||||
Hosted unsubscribe / preferences pages and the URL template variables.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -6,51 +6,108 @@ icon: Layers
|
||||
|
||||
Segments let you create named groups of contacts that can be targeted in campaigns and used as triggers in workflows. There are two types: **Dynamic** and **Static**.
|
||||
|
||||
## Dynamic segments
|
||||
## Dynamic vs Static
|
||||
|
||||
Dynamic segments evaluate a set of filter conditions against your contacts in real time. Membership is kept up to date automatically as contact data and events change — no manual work required.
|
||||
| | Dynamic | Static |
|
||||
| ---------------------------- | ---------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| Membership | Computed from filter conditions in real time | Manually curated — you decide who is in |
|
||||
| `condition` field | Required — describes the filters | Must be omitted |
|
||||
| Add/remove members via API | Not allowed (the filter decides) | `POST` / `DELETE` `/segments/:id/members` |
|
||||
| Membership recomputation | Re-evaluated in the background when tracked | N/A — membership only changes when you call the API |
|
||||
| Entry/exit events | Fired only when **Track membership changes** is enabled | Not fired by add/remove API calls |
|
||||
| Updating the filter | Member count is recomputed | `condition` is silently ignored on update |
|
||||
|
||||
You can filter on:
|
||||
- Contact fields (`email`, `subscribed`, custom data fields like `data.plan`)
|
||||
- Contact dates (`createdAt`, `updatedAt`)
|
||||
- Custom events (`event.signed_up`, `event.purchased`, …)
|
||||
- Email activity (`email.opened`, `email.clicked`, `email.bounced`, …)
|
||||
Use dynamic segments for behavioural targeting ("subscribed users on the Pro plan who opened any email in the last 14 days"). Use static segments for one-off curated lists like beta testers, conference attendees, or contacts imported from an external system.
|
||||
|
||||
Conditions can be combined with `AND`/`OR` logic and nested into groups for complex rules.
|
||||
## Filtering on a dynamic segment
|
||||
|
||||
A dynamic segment's membership is defined by a filter — a set of conditions evaluated against your contacts. You can filter on:
|
||||
|
||||
- Built-in contact fields: `email`, `subscribed`, `createdAt`, `updatedAt`.
|
||||
- Custom fields you've stored on contacts (anything under `data.*`).
|
||||
- Custom events tracked via `/v1/track` (`event.signed_up`, `event.purchased`, etc.).
|
||||
- Email engagement (`email.opened`, `email.clicked`, `email.bounced`, etc.).
|
||||
- Membership of another segment.
|
||||
|
||||
Filters can be combined with `AND` or `OR` and nested into groups for more complex audiences — for example "subscribed Pro users **and** (opened **or** clicked an email in the last 14 days)".
|
||||
|
||||
For the full list of fields, operators, and value types — plus worked examples — see the [Segment filter reference](/guides/segment-filters).
|
||||
|
||||
## Static segments
|
||||
|
||||
Static segments are manually curated lists. Membership does not change automatically — you decide exactly who is in the segment. This is useful for things like beta testers, event attendees, or any group imported from an external source.
|
||||
Static segments are manually curated lists. Membership doesn't change automatically — you decide exactly who is in.
|
||||
|
||||
## Creating a segment
|
||||
### Creating
|
||||
|
||||
Go to **Segments** in the dashboard and click **Create Segment**. Use the toggle at the top to choose **Dynamic** or **Static**.
|
||||
Go to **Segments** in the dashboard and click **Create Segment**, then choose **Static**. You can optionally add initial members straight away using the contact search.
|
||||
|
||||
**For dynamic segments**, use the filter builder to define your conditions. Plunk will show you a live count of matching contacts.
|
||||
### Adding and removing members
|
||||
|
||||
**For static segments**, you can optionally add initial members right away using the contact search. Start typing an email address and select contacts from the list — selected contacts appear as chips you can remove before saving.
|
||||
Open a static segment and use the **Add Members** search to find contacts. The search looks up contacts already in your project, so you can't accidentally add someone who doesn't exist. Contacts already in the segment are greyed out.
|
||||
|
||||
## Managing static segment members
|
||||
Programmatically, use:
|
||||
|
||||
Open a static segment and use the **Add Members** search to find and select contacts. The search looks up contacts already in your project, so you can't accidentally add someone who doesn't exist. Contacts already in the segment are greyed out.
|
||||
- `POST /segments/:id/members` — add contacts by email. Body: `{ emails: string[], createMissing?: boolean, subscribed?: boolean }`. With `createMissing: true`, contacts that don't exist yet are created (and start subscribed unless you pass `subscribed: false`). The response reports `{ added, created, notFound }`.
|
||||
- `DELETE /segments/:id/members` — remove contacts by email. Body: `{ emails: string[] }`. Returns `{ removed }`.
|
||||
|
||||
To remove a member, click the remove button on their row in the members list.
|
||||
Both endpoints **only** accept static segments. Calling them on a dynamic segment returns `400`.
|
||||
|
||||
## Track membership changes
|
||||
Membership changes via these API calls are immediate but **do not** fire `entry`/`exit` events — those are reserved for dynamic, tracked segments.
|
||||
|
||||
Both segment types support **Track membership changes**. When enabled, Plunk fires a webhook event each time a contact enters or leaves the segment:
|
||||
## Tracking membership changes
|
||||
|
||||
- `segment.trial-users.entry` — contact joined the segment
|
||||
- `segment.trial-users.exit` — contact left the segment
|
||||
Dynamic segments can opt into **Track membership changes**. When enabled, Plunk fires events whenever a contact enters or leaves the segment:
|
||||
|
||||
Where `trial-users` is derived from the segment name. See [Webhooks](/guides/webhooks) for the full event payload.
|
||||
- `segment.<slug>.entry` — contact joined
|
||||
- `segment.<slug>.exit` — contact left
|
||||
|
||||
The `<slug>` is derived from the segment name: lowercased, accents and punctuation stripped, whitespace replaced with hyphens, repeated hyphens collapsed. `"VIP Customers"` becomes `segment.vip-customers.entry`. Pick segment names that produce stable slugs — renaming a segment changes the event name.
|
||||
|
||||
Use these events to drive workflows (welcome a contact when they enter a "Trial users" segment, send a re-engagement email when they exit "Active users", etc.). See [Webhooks](/guides/webhooks) for the payload format.
|
||||
|
||||
### How tracking works
|
||||
|
||||
The member count updates immediately when you create or change a dynamic segment's filter. After that, Plunk recomputes membership in the background on a regular cadence — diffing current matches against the previous set, recording entries and exits, and emitting the corresponding events.
|
||||
|
||||
Because of this background cadence, the `memberCount` shown in the dashboard can lag the live state by a few minutes. If you need a fresh value or want to drive a workflow off `entry` / `exit` immediately:
|
||||
|
||||
- `POST /segments/:id/refresh` — force a count refresh (cheap, no events).
|
||||
- `POST /segments/:id/compute` — force a full membership recomputation, which fires any pending entry/exit events.
|
||||
|
||||
## Using segments
|
||||
|
||||
Segments can be used in:
|
||||
- Targeting contacts in [email campaigns](/concepts/campaigns)
|
||||
- Triggering workflows in [marketing automation](/concepts/workflows)
|
||||
Segments can be used to:
|
||||
|
||||
## Managing members via API
|
||||
- Target contacts in [email campaigns](/concepts/campaigns) (set the campaign audience to a segment)
|
||||
- Trigger workflows in [marketing automation](/concepts/workflows) (use the `segment.<slug>.entry` event)
|
||||
|
||||
If you need to manage static segment membership programmatically, use the `POST /segments/:id/members` and `DELETE /segments/:id/members` endpoints. See the [API reference](/api-reference/overview#segments) for details.
|
||||
## Performance notes
|
||||
|
||||
- Date filters on `data.*` rely on ISO 8601 string ordering — store dates as ISO 8601 strings (`2026-05-06T12:00:00Z`) rather than Unix timestamps if you want to use `within` / `olderThan` on them.
|
||||
- Nesting many untracked dynamic segments inside one another increases evaluation cost. If a segment is referenced by others, enable **Track membership** on it so its members are looked up directly instead of recomputed each time.
|
||||
- The cached `memberCount` may be a few minutes behind reality. Treat it as approximate; use `POST /segments/:id/refresh` to force a refresh if you need an exact count.
|
||||
|
||||
## Deleting a segment
|
||||
|
||||
Deleting a segment that's referenced by an active campaign (in `DRAFT`, `SCHEDULED`, or `SENDING` state) returns `409 Conflict`. Cancel the campaign or pick a different audience first.
|
||||
|
||||
## API reference
|
||||
|
||||
See the segments API endpoints in the [API overview](/api-reference/overview#segments) — list, get, create, update, delete, list members, add/remove static members, refresh count, and compute membership.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Filter reference" href="/guides/segment-filters">
|
||||
Deep-link reference for every filter field, operator, and value type.
|
||||
</Card>
|
||||
<Card title="Custom fields" href="/guides/custom-fields">
|
||||
How `data.*` fields are typed and used in filters.
|
||||
</Card>
|
||||
<Card title="Campaigns" href="/concepts/campaigns">
|
||||
Use a segment as a campaign audience.
|
||||
</Card>
|
||||
<Card title="Workflows" href="/concepts/workflows">
|
||||
Trigger workflows from segment entry / exit events.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -4,7 +4,33 @@ description: Create reusable email templates for your campaigns, workflows and t
|
||||
icon: SwatchBook
|
||||
---
|
||||
|
||||
Templates are stored in the Plunk dashboard and can be used in campaigns, workflows and transactional emails.
|
||||
Templates are stored in the Plunk dashboard and can be used in campaigns, workflows, and transactional emails sent through `/v1/send`.
|
||||
|
||||
## Anatomy of a template
|
||||
|
||||
A template bundles everything needed to render an email: content, sender, and behaviour.
|
||||
|
||||
import {TypeTable} from 'fumadocs-ui/components/type-table';
|
||||
|
||||
<TypeTable
|
||||
type={{
|
||||
name: { type: 'string', required: true, description: 'Internal name shown in the dashboard.' },
|
||||
description: { type: 'string', description: 'Optional internal note.' },
|
||||
subject: { type: 'string', required: true, description: 'Email subject line. Supports template variables.' },
|
||||
body: { type: 'string', required: true, description: 'HTML body. Supports template variables.' },
|
||||
from: { type: 'string', required: true, description: 'Sender email — must be on a verified domain.' },
|
||||
fromName: { type: 'string', description: 'Optional sender display name.' },
|
||||
replyTo: { type: 'string', description: 'Optional reply-to address.' },
|
||||
type: { type: 'string', required: true, description: 'One of `MARKETING`, `TRANSACTIONAL`, or `HEADLESS`. See [Template types](#templates-types).' },
|
||||
style: { type: 'object', description: "Editor style metadata used by the dashboard's drag-and-drop editor. You can ignore this when creating templates via the API." },
|
||||
}}
|
||||
/>
|
||||
|
||||
When a template is used by `/v1/send`, the request can override any of `subject`, `body`, `from`, `fromName`, `replyTo` — the template's values act as defaults.
|
||||
|
||||
### Sender domain verification
|
||||
|
||||
The `from` address must be on a verified domain. Creating or updating a template with an unverified `from` returns `403`. See [Verifying domains](/guides/verifying-domains).
|
||||
|
||||
## Designing templates
|
||||
|
||||
@@ -49,6 +75,11 @@ The following fields are reserved by Plunk but can still be used.
|
||||
|
||||
You can preview your templates by selecting a contact in the preview window. This will allow you to see how the template will look for that specific contact, with their data populated.
|
||||
|
||||
## Duplicating and finding usage
|
||||
|
||||
- `POST /templates/:id/duplicate` — creates an editable copy. Useful for A/B variants or as a starting point for related emails.
|
||||
- `GET /templates/:id/usage` — lists every campaign, workflow step, and other reference that points at this template. Use this to answer "what breaks if I change this template?" or to find leftover references before deletion.
|
||||
|
||||
## Templates types
|
||||
|
||||
There are three types of templates in Plunk. Each type is treated at the same priority when sending emails, you should not pick one type over the other based on deliverability or performance.
|
||||
@@ -58,3 +89,20 @@ There are three types of templates in Plunk. Each type is treated at the same pr
|
||||
| Marketing | Yes | Yes | Automatically includes a Plunk-hosted unsubscribe footer. Will not be sent to contacts who are unsubscribed |
|
||||
| Transactional | No | No | Does not include any way to unsubscribe. Will be sent to any contact, regardless of subscription state |
|
||||
| Headless | Yes | No | Respects opt-out like marketing, but no Plunk footer is appended. You are responsible for providing an unsubscribe mechanism in the email body. Use `{{unsubscribeUrl}}` or `{{manageUrl}}` to link to Plunk's managed unsubscribe page |
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Transactional emails" href="/concepts/transactional-emails">
|
||||
Send templated emails directly through `/v1/send`.
|
||||
</Card>
|
||||
<Card title="Campaigns" href="/concepts/campaigns">
|
||||
Broadcast a template to a segment or audience.
|
||||
</Card>
|
||||
<Card title="Workflows" href="/concepts/workflows">
|
||||
Send templates as part of automated journeys.
|
||||
</Card>
|
||||
<Card title="Localization" href="/guides/localization">
|
||||
Translate the unsubscribe footer and contact-facing pages.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
@@ -9,12 +9,24 @@ Transactional emails are emails sent directly through the API. They are typicall
|
||||
## Sending with attachments
|
||||
Plunk supports sending attachments with transactional emails. By default, you can include up to 10 attachments per email with a maximum total size of 10MB. Attachments should be base64 encoded and included in the `attachments` array when sending the email via the [/v1/email/send](/api-reference/public-api/sendEmail) endpoint.
|
||||
|
||||
Self-hosters can raise these limits via environment variables to match the capacity of the underlying email provider (AWS SES supports up to 40MB per message by default):
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `MAX_ATTACHMENT_SIZE_MB` | `10` | Maximum total attachment size in megabytes |
|
||||
| `MAX_ATTACHMENTS_COUNT` | `10` | Maximum number of attachments per email |
|
||||
The total message size cannot exceed 40 MB. Self-hosters can adjust the defaults — see [Environment variables](/self-hosting/environment-variables).
|
||||
|
||||
## Sending from a template
|
||||
You can also send transactional emails using a [template](/concepts/templates) you have created in the dashboard. This allows you to reuse the same design and content for multiple emails, while still personalizing them with contact data.
|
||||
You can also send transactional emails using a [template](/concepts/templates) you have created in the dashboard. This allows you to reuse the same design and content for multiple emails, while still personalizing them with contact data.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Send Email API" href="/api-reference/public-api/sendEmail">
|
||||
Full reference for `POST /v1/send`.
|
||||
</Card>
|
||||
<Card title="Templates" href="/concepts/templates">
|
||||
Author reusable email designs.
|
||||
</Card>
|
||||
<Card title="API keys" href="/guides/api-keys">
|
||||
Authenticating requests with `sk_*` and `pk_*` keys.
|
||||
</Card>
|
||||
<Card title="Webhooks" href="/guides/webhooks">
|
||||
Track delivery, opens, clicks, and bounces of your sends.
|
||||
</Card>
|
||||
</Cards>
|
||||
@@ -1,41 +1,100 @@
|
||||
---
|
||||
title: Workflows
|
||||
description: Set up automated email sequences and trigger them from your apps
|
||||
description: Build automated, multi-step contact journeys triggered by events, segments, schedules, or manual entry
|
||||
icon: Workflow
|
||||
---
|
||||
|
||||
Workflows in Plunk allow you to create automated email sequences that can be triggered based on events you send using the API.
|
||||
Workflows are graph-based automations that move contacts through a sequence of steps — sending emails, waiting for activity, branching on conditions, calling webhooks, updating contact data, or exiting. Each contact that enters a workflow runs through it independently as a **workflow execution**.
|
||||
|
||||
## Prerequisites for workflows
|
||||
### Sending an event
|
||||
Workflows are triggered by sending events to Plunk using the [/v1/track](/api-reference/public-api/trackEvent) endpoint. When sending an event, you can specify the contact it is associated with and include any relevant data.
|
||||
## Triggers
|
||||
|
||||
### Creating a template
|
||||
Before setting up the workflow, ensure you have created a template that will be used for the emails sent by the workflow. You can create templates in the [Templates](/concepts/templates) section of the dashboard.
|
||||
A workflow has a single **trigger** that decides how contacts enter:
|
||||
|
||||
## Creating a workflow
|
||||
To create a workflow, navigate to the [Workflows](/concepts/workflows) section of the dashboard and create a workflow.
|
||||
| Trigger type | When it fires |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| `EVENT` | Any time a matching event is tracked on a contact (custom event via `/v1/track`, system events such as `email.received` or `contact.subscribed`, or segment entry/exit events). |
|
||||
| `MANUAL` | Only when you explicitly start an execution via `POST /workflows/:id/executions`. |
|
||||
| `SCHEDULE` | On a recurring schedule defined by `triggerConfig` (e.g. cron expression). |
|
||||
|
||||
<Callout
|
||||
title="Trigger"
|
||||
variant="idea">
|
||||
When creating a workflow, you can not change the trigger event after the workflow has been created. Make sure to choose the correct event name that will trigger the workflow.
|
||||
</Callout>
|
||||
When you create a workflow via the API, the trigger defaults to `EVENT` with the event name you provide. To use `MANUAL` or `SCHEDULE`, change `triggerType` and `triggerConfig` with `PATCH /workflows/:id` after creation.
|
||||
|
||||
### Defining workflow steps
|
||||
Workflows consists of multiple steps that define the sequence of actions to be taken.
|
||||
A workflow's trigger event name **cannot be changed after the first execution** — pick it carefully. Other trigger configuration (delays, conditions inside steps) remains editable.
|
||||
|
||||
| Step Type | Description |
|
||||
|-----------|-------------|
|
||||
| Send Email | Sends an email to the contact using a specified template. You can customize the email content using variables from the event data. |
|
||||
| Delay | Pauses the workflow for a specified duration before proceeding to the next step. |
|
||||
| Wait for Event | Pauses the workflow until a specified event is received for the contact. You can also set a timeout duration to proceed if the event is not received within that time. |
|
||||
| Condition | Evaluates a condition based on the event data or contact data and branches the workflow accordingly. |
|
||||
| Webhook | Sends a webhook to a specified URL with the event and contact data. |
|
||||
| Update Contact | Updates the contact's data with specified key-value pairs. |
|
||||
| Exit | Terminates the workflow for the contact. |
|
||||
## Lifecycle and the `enabled` flag
|
||||
|
||||
## Managing workflow executions
|
||||
You can monitor and manage contacts going through workflows in the executions tab of the workflow detail page. Here you can see the status of each execution, cancel a specific execution or all executions.
|
||||
Workflows are created with `enabled: false`. **No executions start until you flip `enabled` to `true`** — this is the most common reason "my workflow isn't firing."
|
||||
|
||||
A workflow will be locked while there are active executions. If you want to make changes to a running workflow you will either need to pause it and wait for all executions to complete, or cancel all active executions.
|
||||
The `allowReentry` flag (default `false`) controls whether a contact can enter the same workflow more than once. Leave it off for "first-touch only" sequences (welcome emails, onboarding) and turn it on for re-engageable journeys (re-prompts, repeated nudges).
|
||||
|
||||
## Step types
|
||||
|
||||
A workflow always begins with a single auto-created `TRIGGER` step. You build the rest of the graph by adding steps and **transitions** (edges) between them.
|
||||
|
||||
| Step type | Purpose | Required config |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| `TRIGGER` | Auto-created entry point. Holds the trigger configuration. | `eventName` (for `EVENT` trigger) |
|
||||
| `SEND_EMAIL` | Sends an email to the contact using a template. Template variables resolve from contact data + execution context. | `templateId`, optional `from` override |
|
||||
| `DELAY` | Pauses the execution for a fixed duration before continuing. | `amount`, `unit` (`minutes` / `hours` / `days`) |
|
||||
| `WAIT_FOR_EVENT` | Pauses until a specified event is tracked on the contact, with a timeout fallback. | `eventName`, `timeout` (seconds) |
|
||||
| `CONDITION` | Branches the execution based on contact data or event data. Each `CONDITION` step has two outgoing transitions tagged `yes` / `no`. | A filter expression (same shape as segment filters) |
|
||||
| `WEBHOOK` | Calls an external HTTPS endpoint with contact + execution context as the JSON body. | `url`, optional `method`, `headers` |
|
||||
| `UPDATE_CONTACT` | Patches contact data — useful for tagging contacts as they progress (`{ stage: "activated" }`). | `data` object |
|
||||
| `EXIT` | Terminates the execution. Optionally records an `exitReason` for analytics. | optional `reason` |
|
||||
|
||||
## Transitions
|
||||
|
||||
Transitions are the edges of the workflow graph. For most step types a transition is a simple "next" pointer. For `CONDITION` steps, each transition carries a `branch` discriminator (`"yes"` or `"no"`) so the engine knows which path to take when the condition evaluates.
|
||||
|
||||
When deleting a step in the middle of a chain, pass `?splice=true` on `DELETE /workflows/:id/steps/:stepId` to automatically reconnect the surrounding transitions; otherwise the deletion leaves the graph disconnected.
|
||||
|
||||
## Executions
|
||||
|
||||
Each contact entering the workflow creates a `WorkflowExecution`. Executions move through these states:
|
||||
|
||||
| Status | Meaning |
|
||||
| ----------- | --------------------------------------------------------------------------------------------- |
|
||||
| `RUNNING` | Currently processing a step (or about to). |
|
||||
| `WAITING` | Paused inside a `DELAY` or `WAIT_FOR_EVENT` step. |
|
||||
| `COMPLETED` | Reached the end of the graph successfully. |
|
||||
| `EXITED` | Hit an `EXIT` step. `exitReason` records why. |
|
||||
| `FAILED` | An unrecoverable error occurred (e.g. webhook returned non-2xx after retries, template not found). |
|
||||
| `CANCELLED` | Cancelled manually via the API or dashboard. |
|
||||
|
||||
Each execution carries a `context` JSON object that's merged with the contact's `data` when rendering templates and evaluating conditions. When you start an execution manually with `POST /workflows/:id/executions`, you can pass an initial `context` — this is how you parameterise per-execution variables (a coupon code, a referrer name) without storing them on the contact.
|
||||
|
||||
## Locking active workflows
|
||||
|
||||
A workflow is **locked** for structural edits while it has active (`RUNNING` or `WAITING`) executions. To make breaking changes — adding/removing steps, changing the trigger event, switching trigger type — first either:
|
||||
|
||||
- Disable the workflow and wait for active executions to complete, or
|
||||
- `POST /workflows/:id/executions/cancel-all` to cancel everything in flight.
|
||||
|
||||
Cosmetic changes (renaming, editing template content referenced by steps) don't require this.
|
||||
|
||||
## Common patterns
|
||||
|
||||
- **Welcome series** — `EVENT` trigger on `signed_up`, send welcome email, delay 2 days, send tips email, delay 5 days, send upgrade nudge.
|
||||
- **Inbound auto-reply** — `EVENT` trigger on `email.received`, `CONDITION` on `event.spamVerdict == "PASS"`, then `SEND_EMAIL` with an auto-reply template. See [Receiving emails](/guides/receiving-emails).
|
||||
- **Re-engagement** — Triggered by a segment exit (`segment.active-users.exit`), send a "we miss you" email, wait 7 days for any `email.opened` event, branch on whether the contact engaged.
|
||||
- **Webhook fan-out** — `EVENT` trigger on `purchase.completed`, `WEBHOOK` step to your CRM, `UPDATE_CONTACT` to tag `{ tier: "customer" }`, `SEND_EMAIL` with the receipt.
|
||||
|
||||
## API reference
|
||||
|
||||
Workflows are managed through the `/workflows` API. See the [API overview](/api-reference/overview#workflows) for the full route list — the API exposes endpoints for the workflow itself, its steps, its transitions, and its executions.
|
||||
|
||||
## What's next
|
||||
|
||||
<Cards>
|
||||
<Card title="Templates" href="/concepts/templates">
|
||||
Author the email content used by `SEND_EMAIL` steps.
|
||||
</Card>
|
||||
<Card title="Receiving emails" href="/guides/receiving-emails">
|
||||
Trigger workflows from inbound mail with `email.received`.
|
||||
</Card>
|
||||
<Card title="Webhooks" href="/guides/webhooks">
|
||||
Use `WEBHOOK` steps to call your own services from a workflow.
|
||||
</Card>
|
||||
<Card title="Segments" href="/concepts/segments">
|
||||
Trigger workflows from segment entry / exit events.
|
||||
</Card>
|
||||
</Cards>
|
||||
|
||||
Reference in New Issue
Block a user