# Export audit logs
Source: https://docs.notifuse.com/api-reference/audit-logs/export-audit-logs
/openapi.json post /api/auditLogs.export
Streams the entries the filter selects, newest first, as a CSV or NDJSON file download, up to 100 000 rows. When the cap is reached the response ends with an export.truncated marker row; narrow the date range for the rest. The same authorisation as auditLogs.list applies, before the first byte is written. The export is itself recorded as auditLogs.export.
# Get an audit log entry
Source: https://docs.notifuse.com/api-reference/audit-logs/get-an-audit-log-entry
/openapi.json get /api/auditLogs.get
Returns one entry by id. In workspace scope an entry of another workspace is a 404, never a leak.
# List audit actions
Source: https://docs.notifuse.com/api-reference/audit-logs/list-audit-actions
/openapi.json get /api/auditLogs.actions
The catalogue of every action the log can contain, sorted by name, with its category. The console builds its action filter from it.
# List audit logs
Source: https://docs.notifuse.com/api-reference/audit-logs/list-audit-logs
/openapi.json get /api/auditLogs.list
Returns one page of audit entries, newest first. With scope=workspace (the default) workspace_id is required and the caller must be an owner of that workspace or hold the audit_logs read permission; with scope=deployment the caller must be a root user and every entry is returned, optionally narrowed to one workspace.
Never refused for licence reasons: the page works in every licence state on whatever was recorded while a key covered audit logs.
# Set the workspace audit log retention
Source: https://docs.notifuse.com/api-reference/audit-logs/set-the-workspace-audit-log-retention
/openapi.json post /api/workspaces.setAuditLogSettings
Owners only. retention_days absent means the deployment default; 0 keeps every entry forever; otherwise 30 to 3650. Works in every licence state.
# Root user programmatic signin
Source: https://docs.notifuse.com/api-reference/authentication/root-user-programmatic-signin
/openapi.json post /api/user.rootSignin
Authenticates the root user using HMAC signature for programmatic access.
This endpoint is designed for automation scenarios like Infrastructure-as-Code
deployments, CI/CD pipelines, and automated testing where magic link
authentication is impractical.
**Security Features:**
- HMAC-SHA256 signature verification using the application's secret key
- 60-second timestamp window to prevent replay attacks
- Rate limited to 5 attempts per 5 minutes per email
- Only works for a configured root email address (ROOT_EMAIL may list several, comma/semicolon-separated)
**How to generate the signature:**
```bash
SECRET_KEY="your-notifuse-secret-key"
ROOT_EMAIL="admin@example.com"
TIMESTAMP=$(date +%s)
MESSAGE="${ROOT_EMAIL}:${TIMESTAMP}"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET_KEY" | awk '{print $2}')
```
# Batch import contacts
Source: https://docs.notifuse.com/api-reference/batch-import-contacts
/openapi.json post /api/contacts.import
Creates or updates multiple contacts in a single batch operation. This is significantly more efficient than individual upsert operations. Optionally subscribes all contacts to specified lists.
# Create a blog category
Source: https://docs.notifuse.com/api-reference/blog/create-a-blog-category
/openapi.json post /api/blogCategories.create
Creates a category. Posts always belong to one, so a blog needs at least one category before its first post. Requires the blog write permission.
# Create a blog post
Source: https://docs.notifuse.com/api-reference/blog/create-a-blog-post
/openapi.json post /api/blogPosts.create
Creates a post as a draft. The post holds the metadata; its body lives in a web template, so publishing a post over the API takes three calls:
1. `templates.create` with `channel: "web"`, `category: "blog"` and a `web` object. `web.html` is the body readers see, rendered as-is. `web.content` is required and is the Tiptap document the console editor opens; if you only have HTML, send `{"type": "doc", "content": []}`, and do not edit that post in the console afterwards, because saving there regenerates `web.html` from `web.content`.
2. `blogPosts.create` with that template's `id` as `template_id`.
3. `blogPosts.publish`.
Requires the blog write permission, plus the templates write permission for step 1.
# Create a blog theme version
Source: https://docs.notifuse.com/api-reference/blog/create-a-blog-theme-version
/openapi.json post /api/blogThemes.create
Saves a new, unpublished theme version, numbered one above the highest existing version. Files left out are stored empty. Nothing changes on the blog until the version is published. Requires the blog write permission.
# Delete a blog category
Source: https://docs.notifuse.com/api-reference/blog/delete-a-blog-category
/openapi.json post /api/blogCategories.delete
Deletes a category **and every post in it**, in one transaction. Both are soft-deleted, which frees their slugs for reuse. Public pages are cached for up to 5 minutes, so the category can take that long to disappear from the blog. Requires the blog write permission.
# Delete a blog post
Source: https://docs.notifuse.com/api-reference/blog/delete-a-blog-post
/openapi.json post /api/blogPosts.delete
Soft-deletes a post, which takes it off the blog and frees its slug. Its template is left in place. Requires the blog write permission.
# Get a blog category
Source: https://docs.notifuse.com/api-reference/blog/get-a-blog-category
/openapi.json get /api/blogCategories.get
Retrieves a category by `id` or by `slug`; when both are given, `id` wins. Requires the blog read permission.
# Get a blog post
Source: https://docs.notifuse.com/api-reference/blog/get-a-blog-post
/openapi.json get /api/blogPosts.get
Retrieves a post by `id` or by `slug`; when both are given, `id` wins. Drafts are returned too. Requires the blog read permission.
# Get a blog theme version
Source: https://docs.notifuse.com/api-reference/blog/get-a-blog-theme-version
/openapi.json get /api/blogThemes.get
Retrieves one theme version with its files. Requires the blog read permission.
# Get the published blog theme
Source: https://docs.notifuse.com/api-reference/blog/get-the-published-blog-theme
/openapi.json get /api/blogThemes.getPublished
Retrieves the theme version currently live on the blog. Requires the blog read permission.
# List blog categories
Source: https://docs.notifuse.com/api-reference/blog/list-blog-categories
/openapi.json get /api/blogCategories.list
Returns every category of the workspace's blog, newest first. Not paginated. Requires the blog read permission.
# List blog posts
Source: https://docs.notifuse.com/api-reference/blog/list-blog-posts
/openapi.json get /api/blogPosts.list
Returns posts page by page, drafts included unless filtered out. Posts come newest-created first; with `status=published`, most recently published first. Page through with `page` and `limit` until you have `total_count` posts. Requires the blog read permission.
# List blog theme versions
Source: https://docs.notifuse.com/api-reference/blog/list-blog-theme-versions
/openapi.json get /api/blogThemes.list
Returns the workspace's theme versions, newest first, files included. Requires the blog read permission.
# Publish a blog post
Source: https://docs.notifuse.com/api-reference/blog/publish-a-blog-post
/openapi.json post /api/blogPosts.publish
Publishes a draft: it appears on the blog's home and category pages, at its own URL, and in the RSS and JSON feeds. Publishing a post that is already published is refused; unpublish it first to change its date. Requires the blog write permission.
# Publish a blog theme version
Source: https://docs.notifuse.com/api-reference/blog/publish-a-blog-theme-version
/openapi.json post /api/blogThemes.publish
Makes a theme version the one the blog renders, and unpublishes the previous one in the same transaction. Any version can be published, so rolling back is publishing an older one. Requires the blog write permission.
# Unpublish a blog post
Source: https://docs.notifuse.com/api-reference/blog/unpublish-a-blog-post
/openapi.json post /api/blogPosts.unpublish
Turns a published post back into a draft and clears its publication date. Requires the blog write permission.
# Update a blog category
Source: https://docs.notifuse.com/api-reference/blog/update-a-blog-category
/openapi.json post /api/blogCategories.update
Replaces a category's name, slug, description and SEO settings. This is not a patch: optional fields left out are cleared. Public pages are cached for up to 5 minutes, so a rename can take that long to show on the blog. Requires the blog write permission.
# Update a blog post
Source: https://docs.notifuse.com/api-reference/blog/update-a-blog-post
/openapi.json post /api/blogPosts.update
Replaces a post's metadata. This is not a patch: optional fields left out are cleared, so send the post back with your changes applied.
To change the body, update the template with `templates.update`, which saves a new template version. A post pinned to a `template_version` keeps rendering the old one until you update the post with the new version; a post at version `0` follows the latest version on its own, within the 5-minute page cache.
Requires the blog write permission.
# Update a blog theme version
Source: https://docs.notifuse.com/api-reference/blog/update-a-blog-theme-version
/openapi.json post /api/blogThemes.update
Edits an unpublished theme version in place. Only the files named in `files` are replaced. The published version cannot be edited: create a new version, or edit an older one, and publish that instead. Requires the blog write permission.
# Cancel a broadcast
Source: https://docs.notifuse.com/api-reference/cancel-a-broadcast
/openapi.json post /api/broadcasts.cancel
Cancels a scheduled broadcast.
# Compile template
Source: https://docs.notifuse.com/api-reference/compile-template
/openapi.json post /api/templates.compile
Compiles an MJML template with provided data, returning the rendered HTML.
When `subject` and/or `subject_preview` are provided, they are rendered
through the same Liquid engine used at send time and returned alongside
the HTML so callers do not need to run a separate Liquid pass to preview
the subject line. Useful for previewing templates before saving.
# Count contacts in a workspace
Source: https://docs.notifuse.com/api-reference/count-contacts-in-a-workspace
/openapi.json get /api/contacts.count
Returns the total number of contacts in a workspace.
# Create a broadcast
Source: https://docs.notifuse.com/api-reference/create-a-broadcast
/openapi.json post /api/broadcasts.create
Creates a new broadcast campaign in draft status.
**Important:** Creating a broadcast does not schedule or send it. After creating the broadcast,
you must call the `/api/broadcasts.schedule` endpoint to schedule or send it immediately.
# Create a template
Source: https://docs.notifuse.com/api-reference/create-a-template
/openapi.json post /api/templates.create
Creates a new template. Each template must have a channel (email or web) with corresponding channel-specific content.
# Create a webhook subscription
Source: https://docs.notifuse.com/api-reference/create-a-webhook-subscription
/openapi.json post /api/webhookSubscriptions.create
Creates a new webhook subscription to receive HTTP callbacks when specified events occur in the workspace.
# Create or update a contact
Source: https://docs.notifuse.com/api-reference/create-or-update-a-contact
/openapi.json post /api/contacts.upsert
Creates a new contact or updates an existing one. Contacts are matched **by email address only** — the email is the contact's unique identifier. `external_id` is stored but never used for matching, so sending an existing `external_id` with a new email address creates a second contact.
To change a contact's email address: upsert a contact with the new email and the full profile, re-create its list subscriptions (preserving unsubscribed/bounced/complained statuses — do not reset opt-outs), then delete the contact with the old email using `contacts.delete`. Message history and segment memberships are keyed to the email address and are not transferred.
Returns information about whether the contact was created or updated.
# Delete a broadcast
Source: https://docs.notifuse.com/api-reference/delete-a-broadcast
/openapi.json post /api/broadcasts.delete
Deletes a broadcast. This action is permanent and cannot be undone.
# Delete a contact
Source: https://docs.notifuse.com/api-reference/delete-a-contact
/openapi.json post /api/contacts.delete
Deletes a contact from the workspace by email address. This action is permanent and cannot be undone.
# Delete a template
Source: https://docs.notifuse.com/api-reference/delete-a-template
/openapi.json post /api/templates.delete
Soft-deletes a template. The template will be marked as deleted but not removed from the database.
# Delete a webhook subscription
Source: https://docs.notifuse.com/api-reference/delete-a-webhook-subscription
/openapi.json post /api/webhookSubscriptions.delete
Permanently deletes a webhook subscription. This action cannot be undone.
# Get a broadcast
Source: https://docs.notifuse.com/api-reference/get-a-broadcast
/openapi.json get /api/broadcasts.get
Retrieves a single broadcast by ID. Optionally includes template details for each variation.
# Get a contact by email address
Source: https://docs.notifuse.com/api-reference/get-a-contact-by-email-address
/openapi.json get /api/contacts.getByEmail
Retrieves a contact by their email address within a specific workspace. The response always includes the contact's list subscriptions with their status (active, pending, unsubscribed, bounced, complained).
# Get a contact by external ID
Source: https://docs.notifuse.com/api-reference/get-a-contact-by-external-id
/openapi.json get /api/contacts.getByExternalID
Retrieves a contact by their external ID within a specific workspace. The response always includes the contact's list subscriptions with their status (active, pending, unsubscribed, bounced, complained).
Note that `external_id` is not unique — if several contacts share the same external ID, only one of them is returned (which one is not defined). Contacts are uniquely identified by email address.
# Get a template
Source: https://docs.notifuse.com/api-reference/get-a-template
/openapi.json get /api/templates.get
Retrieves a single template by ID. Optionally specify a version number to retrieve a specific version.
# Get a webhook subscription
Source: https://docs.notifuse.com/api-reference/get-a-webhook-subscription
/openapi.json get /api/webhookSubscriptions.get
Retrieves a specific webhook subscription by ID.
# Get A/B test results
Source: https://docs.notifuse.com/api-reference/get-ab-test-results
/openapi.json get /api/broadcasts.getTestResults
Retrieves the performance metrics for all variations in an A/B test broadcast.
# Get webhook delivery history
Source: https://docs.notifuse.com/api-reference/get-webhook-delivery-history
/openapi.json get /api/webhookSubscriptions.deliveries
Returns the delivery history for a webhook subscription, including status and response information.
# Import custom events
Source: https://docs.notifuse.com/api-reference/import-custom-events
/openapi.json post /api/customEvents.import
Imports multiple custom events in a single batch operation. Maximum 50 events per request. Auto-creates contacts if they don't exist.
# List available event types
Source: https://docs.notifuse.com/api-reference/list-available-event-types
/openapi.json get /api/webhookSubscriptions.eventTypes
Returns a list of all available webhook event types that can be subscribed to.
# List broadcasts
Source: https://docs.notifuse.com/api-reference/list-broadcasts
/openapi.json get /api/broadcasts.list
Retrieves a list of broadcasts with pagination and optional filtering by status. Supports fetching template details for each variation.
# List contacts with filtering and pagination
Source: https://docs.notifuse.com/api-reference/list-contacts-with-filtering-and-pagination
/openapi.json get /api/contacts.list
Retrieves a paginated list of contacts with optional filtering. All contact fields are always returned.
**Filtering**: Use filters to search for contacts. Text filters (email, external_id, first_name, last_name, full_name, phone, country, language) use case-insensitive partial matching (ILIKE).
**List filtering**: Use `list_id` and/or `contact_list_status` to filter contacts by list membership.
**Segment filtering**: Use `segments[]` to filter contacts that belong to specific segments.
**Contact lists**: By default, `contact_lists` is not included in the response. Set `with_contact_lists=true` to include the contact's list subscriptions.
**Pagination**: Uses cursor-based pagination. Use the `next_cursor` from the response to fetch the next page.
# List templates
Source: https://docs.notifuse.com/api-reference/list-templates
/openapi.json get /api/templates.list
Retrieves a list of all templates in the workspace. Supports optional filtering by category and channel.
# List webhook subscriptions
Source: https://docs.notifuse.com/api-reference/list-webhook-subscriptions
/openapi.json get /api/webhookSubscriptions.list
Returns all webhook subscriptions for the specified workspace.
# Pause a broadcast
Source: https://docs.notifuse.com/api-reference/pause-a-broadcast
/openapi.json post /api/broadcasts.pause
Pauses a broadcast that is currently sending.
# Regenerate webhook secret
Source: https://docs.notifuse.com/api-reference/regenerate-webhook-secret
/openapi.json post /api/webhookSubscriptions.regenerateSecret
Generates a new signing secret for the webhook subscription. The old secret will immediately become invalid.
# Resume a broadcast
Source: https://docs.notifuse.com/api-reference/resume-a-broadcast
/openapi.json post /api/broadcasts.resume
Resumes a paused broadcast.
# Schedule a broadcast
Source: https://docs.notifuse.com/api-reference/schedule-a-broadcast
/openapi.json post /api/broadcasts.schedule
Schedules a broadcast for sending either immediately or at a specified time. This endpoint is restricted in demo mode.
# Select winning A/B test variation
Source: https://docs.notifuse.com/api-reference/select-winning-ab-test-variation
/openapi.json post /api/broadcasts.selectWinner
Manually selects the winning variation for an A/B test broadcast. The winning variation will be sent to the remaining audience. This endpoint is restricted in demo mode.
# Send a test webhook
Source: https://docs.notifuse.com/api-reference/send-a-test-webhook
/openapi.json post /api/webhookSubscriptions.test
Sends a test webhook to the subscription's URL to verify connectivity and configuration.
# Send a transactional notification
Source: https://docs.notifuse.com/api-reference/send-a-transactional-notification
/openapi.json post /api/transactional.send
Sends a transactional notification to a contact through specified channels.
Requires authentication.
# Send broadcast to individual
Source: https://docs.notifuse.com/api-reference/send-broadcast-to-individual
/openapi.json post /api/broadcasts.sendToIndividual
Sends a broadcast to a single recipient for testing purposes. Optionally specify which template variation to send.
# Subscribe to email lists
Source: https://docs.notifuse.com/api-reference/subscribe-to-email-lists
/openapi.json post /subscribe
Subscribe a contact to one or more email lists. This is a public endpoint that doesn't require authentication.
**Important:** For unauthenticated requests, only **public lists** can be subscribed to. To subscribe to private lists, you can either:
- Provide a valid `email_hmac` in the contact object (used by the notification center for re-subscribing after unsubscribing)
- Use the authenticated `/api/lists.subscribe` endpoint with a bearer token
If the list has double opt-in enabled, a confirmation email will be sent.
# Subscribe to email lists (authenticated)
Source: https://docs.notifuse.com/api-reference/subscribe-to-email-lists-authenticated
/openapi.json post /api/lists.subscribe
Subscribe a contact to one or more email lists. This is an authenticated endpoint that requires a bearer token.
Unlike the public `/subscribe` endpoint, this endpoint can subscribe contacts to **any list** (public or private) as long as the authenticated user has write permission on lists.
**Note:** Double opt-in is bypassed for authenticated requests. Contacts are immediately set to `active` status regardless of the list's double opt-in setting. This allows administrators to manually add contacts without requiring email confirmation.
# Toggle webhook subscription enabled state
Source: https://docs.notifuse.com/api-reference/toggle-webhook-subscription-enabled-state
/openapi.json post /api/webhookSubscriptions.toggle
Enables or disables a webhook subscription. Disabled subscriptions will not receive events.
# Update a broadcast
Source: https://docs.notifuse.com/api-reference/update-a-broadcast
/openapi.json post /api/broadcasts.update
Updates an existing broadcast. Only broadcasts in draft, scheduled, or paused status can be updated.
# Update a template
Source: https://docs.notifuse.com/api-reference/update-a-template
/openapi.json post /api/templates.update
Updates an existing template. Creates a new version of the template.
# Update a webhook subscription
Source: https://docs.notifuse.com/api-reference/update-a-webhook-subscription
/openapi.json post /api/webhookSubscriptions.update
Updates an existing webhook subscription with new configuration.
# Update contact list subscription status
Source: https://docs.notifuse.com/api-reference/update-contact-list-subscription-status
/openapi.json post /api/contactLists.updateStatus
Updates the subscription status of a contact in a specific list. Can set status to active, pending, unsubscribed, bounced, or complained.
**Note:** This endpoint only modifies **existing** list memberships. It does not add a contact to a list. To add a contact to a list, use the `/subscribe` endpoint (for public lists) or the `/api/contacts.import` endpoint (for any list, requires authentication).
# Cancel the in-flight backfill run
Source: https://docs.notifuse.com/api-reference/web-analytics/cancel-the-in-flight-backfill-run
/openapi.json post /api/webAnalytics.backfillCancel
# Create an annotation
Source: https://docs.notifuse.com/api-reference/web-analytics/create-an-annotation
/openapi.json post /api/annotations.create
Creates a manual annotation. Requires the web_analytics write permission.
`source` is always stored as `manual` and `source_id` is always empty: only the platform writes automatic rows, so there is no way to attach a caller-supplied key to one. **This endpoint is therefore not idempotent** — a retried call (a CI job re-running a deploy marker, for instance) writes a second annotation. De-duplicate on the caller's side, or delete the extra row.
Omitted `color` and `timezone` are filled in server-side: the default colour, and the workspace timezone falling back to UTC.
# Delete an annotation
Source: https://docs.notifuse.com/api-reference/web-analytics/delete-an-annotation
/openapi.json post /api/annotations.delete
Deletes an annotation, automatic ones included — a broadcast annotation cannot come back by accident, since its broadcast has already started. Requires the web_analytics write permission.
# Get an annotation
Source: https://docs.notifuse.com/api-reference/web-analytics/get-an-annotation
/openapi.json get /api/annotations.get
Retrieves a single annotation by ID. Requires the web_analytics read permission.
# Get the latest backfill run
Source: https://docs.notifuse.com/api-reference/web-analytics/get-the-latest-backfill-run
/openapi.json post /api/webAnalytics.backfillStatus
# Ingest a web analytics beat
Source: https://docs.notifuse.com/api-reference/web-analytics/ingest-a-web-analytics-beat
/openapi.json post /track
Public collect endpoint used by the Notifuse Analytics browser SDK. Each beat carries the full cumulative session state (all pageviews and goals), so the server can rebuild the session from any single payload. Beats are typically sent as text/plain to avoid CORS preflights; the body is JSON regardless of Content-Type. Silently-dropped traffic (disabled feature, disallowed origin, bot user agents) still receives success responses.
The endpoint is public and unauthenticated, so the identity fields (contact_email + contact_email_hmac, or identify_token) are claims until they verify against the workspace secret. One that does not verify is not an error: the beat is recorded anonymously. A single malformed action is dropped the same way, leaving the rest of the beat intact.
Verifying the credential is the FIRST of three gates, not the only one. An address that clears all three is attached to the session; failing any of them costs the identity alone, silently — the beat is still stored and the response is still a 200 with no error. Success and every failure are therefore indistinguishable on the wire:
1. Signature (or token decryption) must verify against the workspace secret. The recipe is in the contact_email_hmac schema; getting the domain-separation prefix wrong is the usual cause.
2. Rate limits, applied to the IDENTIFIED path only (anonymous traffic is the normal firehose and stays unthrottled) and before the contact lookup, so an abusive caller cannot spend database reads: 120 identified beats per minute per workspace + address, and 600 per minute per client IP (that one counted across all workspaces, since an office shares one IP) — both sized around the SDK's 10-30s heartbeat. Exceeding either is deliberately never a 429, which the SDK would only queue for a retry that cannot fix it.
3. The address must ALREADY be a contact in the workspace. A signature proves who the caller is, never that the address belongs to anyone, so without this gate a workspace's own signing key could store the email of people who are not contacts, and erasure would be unenforceable — a deleted contact's next beat would re-stamp the address. The answer is cached for 60 seconds, so a freshly created contact can stay unidentified and a deleted one keep resolving for about that long.
This is why a correctly-signed beat for an address that is not a contact is indistinguishable from a bad signature: both return 200 and record the session anonymously.
# List annotations
Source: https://docs.notifuse.com/api-reference/web-analytics/list-annotations
/openapi.json get /api/annotations.list
Returns the workspace's annotations, most recent moment first. Every filter is optional; with none the most recent 100 rows come back.
Gated by the web_analytics read permission, like the rest of the web analytics surface.
# Replace the workspace's web analytics settings
Source: https://docs.notifuse.com/api-reference/web-analytics/replace-the-workspaces-web-analytics-settings
/openapi.json post /api/workspaces.setWebAnalyticsSettings
Gated by the web_analytics write permission (like blog settings, members manage the feature without workspace write access). The attribution filters version is recomputed server-side. Passing null settings clears the configuration.
Writing an identified visitor's goals and navigation to their contact timeline is not configured here and has no setting: calling identify() with an HMAC is the opt-in, because minting that credential requires the workspace secret.
# Start an attribution backfill
Source: https://docs.notifuse.com/api-reference/web-analytics/start-an-attribution-backfill
/openapi.json post /api/webAnalytics.backfillStart
Rewrites historical web_sessions and web_goals rows with the current attribution rules, one monthly partition per step. Fails while a run is already pending or running.
# Update an annotation
Source: https://docs.notifuse.com/api-reference/web-analytics/update-an-annotation
/openapi.json post /api/annotations.update
Edits an annotation's moment, timezone and presentation. Requires the web_analytics write permission.
Automatic annotations are editable — an operator may want to reword a broadcast's title — but their origin is not: `source` and `source_id` are reloaded from storage and carried forward, so an edit can neither promote a manual row to a system one nor take over another broadcast's slot.
# Blog Posts
Source: https://docs.notifuse.com/blog/posts
Create and manage blog posts with categories, scheduling, and SEO.
## Categories
Posts are organized into categories. Each category has:
* **Name**: Display name
* **Slug**: URL identifier (lowercase, hyphens only)
* **Description**: Optional description
* **SEO Settings**: Default SEO for posts in this category
Create categories before adding posts.
## Creating Posts
1. Select a category from the sidebar
2. Click **New Post**
3. Fill in the post details
4. Save as draft or publish
## Post Fields
| Field | Description |
| - | - |
| **Title** | Post headline (max 500 chars) |
| **Slug** | URL identifier (auto-generated from title) |
| **Excerpt** | Short summary for listings |
| **Featured Image** | Cover image URL |
| **Authors** | Author names and avatar URLs |
| **Reading Time** | Estimated minutes to read |
| **Template** | Web template containing the post content |
## Publishing
### Publish Now
Click **Publish** to make the post live immediately.
### Schedule
Set a future `published_at` date to schedule publication:
1. Click **Schedule**
2. Select date and time
3. Choose timezone
4. The post goes live automatically at the scheduled time
### Unpublish
Click **Unpublish** to revert a post to draft status.
## SEO Settings
Each post can override category and blog-level SEO:
| Field | Description |
| - | - |
| **Meta Title** | Title tag (defaults to post title) |
| **Meta Description** | Search result description |
| **OG Title** | Social sharing title |
| **OG Description** | Social sharing description |
| **OG Image** | Social sharing image |
| **Keywords** | Meta keywords |
| **Canonical URL** | Canonical URL override |
| **Meta Robots** | Crawling directives |
SEO inheritance: Post > Category > Blog defaults
## Post URLs
Posts are accessible at:
```
https://blog.example.com/{category-slug}/{post-slug}
```
Example: `https://blog.example.com/announcements/new-feature-launch`
## Deleting Posts
Deleted posts are soft-deleted and can be restored. They're removed from the public blog and sitemap immediately.
# Setup
Source: https://docs.notifuse.com/blog/setup
Enable and configure the blog feature for your workspace.
## Enabling the Blog
1. Go to **Settings** > **Blog**
2. Toggle **Enable Blog**
3. A default theme is automatically created and published
## Custom Domain
The blog is served at the root path `/` of your custom domain. Configure this in **Settings** > **Workspace**:
1. Set your **Custom Endpoint URL** (e.g., `https://blog.example.com`)
2. Point your DNS to your Notifuse instance
3. The blog will be available at `https://blog.example.com/`
When the blog is disabled, the root path redirects to `/console`.
## URL Structure
```
https://blog.example.com/ → Home page
https://blog.example.com/?page=2 → Home page 2
https://blog.example.com/announcements → Category page
https://blog.example.com/announcements/hello → Post page
```
## Blog Settings
| Setting | Description |
| - | - |
| **Title** | Blog title (defaults to workspace name) |
| **Logo URL** | Blog-specific logo |
| **Icon URL** | Favicon for the blog |
| **Home Page Size** | Posts per page on home (default: 20, max: 100) |
| **Category Page Size** | Posts per page on categories (default: 20, max: 100) |
## SEO Settings
Default SEO settings apply to all pages unless overridden at the category or post level:
| Field | Description |
| - | - |
| **Meta Title** | Title tag for search engines |
| **Meta Description** | Description for search results |
| **OG Title** | Open Graph title for social sharing |
| **OG Description** | Open Graph description |
| **OG Image** | Image for social sharing |
| **Keywords** | Meta keywords |
| **Meta Robots** | Crawling directives (e.g., `index,follow`) |
| **Canonical URL** | Canonical URL override |
## Auto-Generated Pages
The blog automatically generates:
* **`/robots.txt`** - Allows all crawlers and references the sitemap
* **`/sitemap.xml`** - Lists all published posts with last modified dates
## Caching
Blog pages are cached for 5 minutes. Cache is automatically cleared when:
* A post is published or unpublished
* A post is created, updated, or deleted
* A theme is published
* A category is modified
# Theme
Source: https://docs.notifuse.com/blog/theme
Customize your blog appearance with Liquid templates.
## Theme Files
Each theme consists of 8 files:
| File | Purpose |
| - | - |
| `home.liquid` | Homepage template |
| `category.liquid` | Category listing page |
| `post.liquid` | Individual post page |
| `header.liquid` | Shared header partial |
| `footer.liquid` | Shared footer partial |
| `shared.liquid` | Reusable components |
| `styles.css` | Custom CSS |
| `scripts.js` | Custom JavaScript |
## Liquid Templating
Themes use [Liquid](https://shopify.github.io/liquid/) templating syntax:
```liquid theme={null}
{{ workspace.blog_title }}
{% for post in posts %}
{{ post.title }}
{{ post.excerpt }}
{% endfor %}
```
## Available Variables
### Global (all pages)
```liquid theme={null}
{{ workspace.id }}
{{ workspace.name }}
{{ workspace.blog_title }}
{{ workspace.logo_url }}
{{ workspace.icon_url }}
{{ base_url }}
{{ current_year }}
{{ theme.version }}
```
### Categories
```liquid theme={null}
{% for category in categories %}
{{ category.id }}
{{ category.slug }}
{{ category.name }}
{{ category.description }}
{% endfor %}
```
### Posts (home and category pages)
```liquid theme={null}
{% for post in posts %}
{{ post.id }}
{{ post.slug }}
{{ post.category_slug }}
{{ post.title }}
{{ post.excerpt }}
{{ post.featured_image_url }}
{{ post.published_at }}
{{ post.reading_time_minutes }}
{% for author in post.authors %}
{{ author.name }}
{{ author.avatar_url }}
{% endfor %}
{% endfor %}
```
### Single Post (post page)
```liquid theme={null}
{{ post.title }}
{{ post.content }}
{{ post.excerpt }}
{{ post.featured_image_url }}
{{ post.published_at }}
{{ post.reading_time_minutes }}
{% for toc in post.table_of_contents %}
{{ toc.id }}
{{ toc.level }}
{{ toc.text }}
{% endfor %}
{{ category.name }}
{{ category.slug }}
```
### Pagination
```liquid theme={null}
{{ pagination.current_page }}
{{ pagination.total_pages }}
{{ pagination.has_next }}
{{ pagination.has_previous }}
{{ pagination.total_count }}
{{ pagination.per_page }}
```
### Newsletter Lists
```liquid theme={null}
{% for list in public_lists %}
{{ list.id }}
{{ list.name }}
{{ list.description }}
{% endfor %}
```
## Useful Filters
| Filter | Description | Example |
| - | - | - |
| `date` | Format dates | `{{ post.published_at \| date: "%B %d, %Y" }}` |
| `truncate` | Limit string length | `{{ post.excerpt \| truncate: 150 }}` |
| `truncatewords` | Limit word count | `{{ post.excerpt \| truncatewords: 25 }}` |
| `upcase` / `downcase` | Change case | `{{ category.name \| upcase }}` |
| `capitalize` | Capitalize first letter | `{{ author.name \| capitalize }}` |
| `strip_html` | Remove HTML tags | `{{ post.content \| strip_html }}` |
| `escape` | Escape HTML entities | `{{ post.title \| escape }}` |
| `url_encode` | Encode for URLs | `{{ post.title \| url_encode }}` |
| `default` | Fallback value | `{{ post.featured_image_url \| default: "/images/placeholder.jpg" }}` |
| `size` | Array/string length | `{{ posts \| size }}` |
| `first` / `last` | Get first/last item | `{{ categories \| first }}` |
| `sort` | Sort array | `{{ posts \| sort: "published_at" }}` |
| `reverse` | Reverse array | `{{ posts \| reverse }}` |
| `where` | Filter array | `{{ posts \| where: "category_id", "news" }}` |
| `map` | Extract property | `{{ posts \| map: "title" }}` |
| `join` | Join array items | `{{ post.tags \| join: ", " }}` |
| `split` | Split string | `{{ "a,b,c" \| split: "," }}` |
| `replace` | Replace text | `{{ post.title \| replace: "-", " " }}` |
| `append` / `prepend` | Add to string | `{{ post.slug \| prepend: "/" }}` |
| `plus` / `minus` | Math operations | `{{ pagination.current_page \| plus: 1 }}` |
| `times` / `divided_by` | Math operations | `{{ post.reading_time_minutes \| times: 60 }}` |
## Partials
### Include (shares parent scope)
```liquid theme={null}
{% include 'header' %}
{% include 'footer' %}
```
### Render (isolated scope)
```liquid theme={null}
{% render 'shared', widget: 'newsletter' %}
{% render 'shared', widget: 'categories' %}
```
## Theme Versioning
Themes are versioned. Each save creates a new version:
* Only one version can be published at a time
* Published themes cannot be edited (create a new version instead)
* Unpublished versions are drafts
## Preview Mode
Preview unpublished themes by adding `?preview_theme_version=X` to any blog URL:
```
https://blog.example.com/?preview_theme_version=5
https://blog.example.com/category/slug?preview_theme_version=5
```
Preview bypasses cache and requires authentication.
# Admins & API Keys
Source: https://docs.notifuse.com/features/admins-api-keys
Invite admins and create API keys scoped to exactly the parts of your workspace they need. Choose the permissions a key carries, change them at any time, and understand what each permission really allows.
## Admin Management
### Inviting Admins
* **Owner Only**: Only the workspace owner can invite new admins
* **Scoped Access**: You pick the permissions the invitation carries, and you can change them later
* **Email Invitation**: Send invitations via email address
The owner holds every permission and cannot be restricted. Everyone else — human admins and API keys alike — holds a set of permissions you choose.
### Permissions
**Self-hosted: saving a permission set that is not full access needs a [licence key](/self-hosting/licence)** — setting a member's permissions, inviting with a restricted set, or creating an API key with a restricted scope. Without one, new members and new keys receive full access. Permissions already granted stay enforced in every licence state, and removing a member is always allowed, because that is what revokes a leaked key. Every Notifuse Cloud plan includes it.
A permission is a pair of switches on one area of the workspace: **read** and **write**. Write does not imply read, and both are off unless you grant them. A section appears in the console sidebar when the member holds either switch on the matching area.
Change a member's permissions from the Team page at any time. The change applies to their very next request.
## API Key Management
### Creating API Keys
* **Owner Only**: Only the workspace owner can create an API key
* **Scoped Access**: Choose the permissions the key carries when you create it, or grant everything
* **One-Time Display**: The token is shown only once, when the key is created
A key created without any permission choice has full workspace access — which is also how keys created before scoped permissions existed keep behaving, until you narrow them. A key created with every permission switched off can still authenticate, and still reaches the handful of endpoints no permission gates: `/api/workspaces.list` returns the workspace record the key belongs to — its settings, custom field labels and the reusable template blocks stored on it — with provider credentials redacted. Everything on the table below is closed to it.
Give each integration its own key with the narrowest set of permissions that makes it work. A key that only sends transactional email needs nothing but Transactional write — see [Transactional API](/features/transactional-api). You do not have to work the scope out for [Zapier](/integrations/zapier): connecting it from **Settings** → **Integrations** creates a key already scoped to Webhook Subscriptions, Contacts and Lists read and write, plus Segments read.
The prefix you choose becomes the key's API email address, which is also the SMTP bridge username. It accepts lowercase letters, digits, hyphens and underscores, up to 64 characters, and each prefix can be used once per Notifuse installation.
### Changing a Key's Scope
You can widen or narrow an existing key at any time from the Team page. The change takes effect immediately, on the key's next request. **The token does not change**, so there is nothing to re-issue and no integration to update — you do not have to touch the application holding the key.
### Security
* **Secure Storage**: Copy and store the token immediately after creation
* **No Recovery**: Lost tokens cannot be retrieved — create a new key
* **HTTPS Required**: All API calls must use secure connections
When a key is refused, the API answers `403` and names the area it was missing — as `contacts`, `webhook_subscriptions` and so on — along with the missing switch. A quota `403` carries neither, so the two are easy to tell apart.
## Revoking a Key
Removing the key is the only way to revoke it, and it is destructive: the token stays valid until
the key is removed, and a removed key cannot be restored or re-issued. Every integration using it
stops working the moment you remove it.
Two consequences worth knowing before an incident rather than during one:
* **Narrowing a key is not revoking it.** A key stripped of every permission still authenticates and still holds whatever access is not permission-gated. Treat it as a way to contain a key, not to kill it.
* **You cannot tell which key leaked.** Keys carry no last-used timestamp and no per-key request log, so if you suspect a token has been exposed but do not have the token itself, nothing in Notifuse identifies which of your keys it is. With several keys in play, the only sound response is to remove all of them, create replacements, and update every integration.
Removing the key from the Team page is not always where you do it: deleting a [Zapier](/integrations/zapier) connection from **Settings** → **Integrations** removes the key it minted, and deleting a workspace removes them all. Removing a Zapier connection's key from Team works too and revokes it just the same, but leaves the connection's card behind pointing at a key that no longer exists.
For that reason, keep the number of keys small, give each one a prefix that names the system holding it, and prefer narrow keys — so that a leak costs you one integration rather than the workspace.
## What Each Permission Allows
When a release introduces a new permission, existing members and API keys are granted it on
upgrade. A key you deliberately narrowed therefore widens with the new area, and stays widened
until you narrow it again. Re-check your scoped keys after each upgrade. A key with *every*
permission switched off is the one exception: the upgrade leaves it alone.
**Audit logs read is the second exception**, and the only permission that works this way: it is
never filled in on upgrade and is not part of full access. Grant it deliberately, to the people
and keys that should read every colleague's email address, IP address and refused actions.
In the console, click a permission's name in the matrix to expand the exact list of endpoints it gates and what each one does. The table below is the summary.
| Permission | Read allows | Write allows |
| - | - | - |
| **Contacts** | View contacts and their full profiles, their timeline, and the custom events recorded against them. Also required to list the addresses inside a list or a segment, and to set `cc` or `bcc` on a transactional send | Create and update contacts, **delete** them, **overwrite every field in bulk** through a CSV or API import, and record custom events |
| **Segments** | View segments and their definitions, and the state of their build tasks. Previewing a segment, or listing the contacts it matches, needs Contacts read as well | Create, edit, delete and rebuild segments, and run, reset or delete their build tasks |
| **Lists** | View lists, their settings and their subscriber counts. Listing the members of a list needs Contacts read as well | Create, edit and delete lists, and **subscribe an address to any list, public or not — creating or overwriting that contact's record in the same call**. Changing or removing an existing subscription needs Contacts write as well |
| **Templates** | View templates in every language and the reusable template blocks, and compile a template with test data | Create, edit and delete templates and reusable blocks. Editing a block changes every template that embeds it |
| **Blog** | View categories, posts including unpublished drafts, and theme versions including their scripts | Create, edit, delete, publish and unpublish posts and categories; **publish a theme**; and change the blog settings, including switching the blog on or off |
| **Broadcasts** | View broadcasts, their configuration and their A/B test results, and the state of their send tasks. Delivery statistics are Message History read, not this | Create and edit broadcasts, refresh and test data feeds, and schedule, **send to the entire audience**, pause, resume, cancel and delete a broadcast, pick the A/B winner, and run or reset the underlying send task |
| **Transactional** | View transactional notification definitions | Create, edit and delete them, and send real email — over the API, over the SMTP bridge, as a template test, and as a provider credentials test. See [the send rules](#transactional-sending-and-contacts) below |
| **Automations** | View automations, their complete node configuration including webhook node URLs and bearer secrets, and one named contact's journey through a flow | Create, edit and delete automations, and activate or pause them |
| **Message History** | View sent messages with their recipient addresses and rendered data, and a broadcast's delivered, open, click, bounce and per-link counts | **Nothing — this verb gates nothing today** |
| **Web Analytics** | View site analytics, the annotations drawn on those charts, and backfill progress | Create, edit and delete annotations, start and cancel the historical backfill, and change the web analytics settings — allowed domains, filters, custom dimension labels and email-link identification |
| **Webhook Subscriptions** | List subscriptions and open one, and read the delivery log including each event payload and the endpoint's response. The signing secret is hidden from everyone but the owner | Create, edit, delete, enable and disable subscriptions, and fire a test delivery |
| **Webhook Events** | View the raw callbacks your email providers send back, including recipient addresses and provider diagnostics | **Nothing — this verb gates nothing today** |
| **Audit logs** | Read the [audit trail](/features/audit-logs) and export it: who did what in the workspace, with the actor's email address, the IP address they came from and the actions they were refused. **Never granted by "full access"** — see the note below | **Nothing — this verb gates nothing today.** Entries are written by the platform, and the retention is an owner-only setting |
| **LLM** | **Nothing — this verb gates nothing today** | Use the AI assistant |
| **Workspace** | Read the workspace record and its settings, and see the whole team — members, owners, API keys and pending invitations. Without it the team list is degraded to your own row rather than refused | Rename the workspace's custom contact field labels, and run, reset or delete an integration sync task. **Nothing else** — see below |
Two endpoint families are shared, and answer to whichever permission matches what you ask for: `/api/analytics.query` checks the permission of each schema you query, and `/api/tasks.*` checks the permission of the task type — Segments for segment builds, Broadcasts for sends, Web Analytics for backfills, Workspace for integration syncs.
### Verbs That Gate Nothing Today
Four switches have no endpoint behind them as the API stands: **LLM read**, **Message History write**, **Webhook Events write** and **Audit logs write**. The console shows all four granted and locked, and you cannot turn them off. That is deliberate: storing one as "off" would be permanent, because filling in a missing permission only ever adds what a grant lacks, so nothing would widen it if a later release gives the verb something real to gate.
Granting them confers nothing. Their neighbours do the work: the AI assistant answers to LLM write, message history is read-only because rows are written by the sending pipeline, inbound provider callbacks arrive on routes that carry no session and consult no permission at all, and the audit trail is written by the platform itself — its one writable setting, the retention, is owner-only and answers to no permission.
### Workspace Write Is Narrower Than It Sounds
The only workspace setting the **Workspace** permission lets you change is the custom contact field labels. It does **not** cover:
* **Renaming the workspace, changing its general settings, or deleting it** — owner-only.
* **Creating, updating or deleting an integration** — owner-only. Granting Workspace write does not confer it, and an owner has it without.
* **Blog settings**, which answer to Blog write.
* **Web analytics settings**, which answer to Web Analytics write.
So handing someone the blog or the site analytics also hands them the workspace-level configuration of that feature — including the switch that turns the feature itself on or off.
## Owner-Only Actions
No permission grants these. They stay with the workspace owner whatever a member or key holds:
* Renaming the workspace, changing its general settings, and deleting it
* Creating, updating and deleting integrations, and the Amazon SES configuration endpoints
* Registering webhooks with an email provider, because it uses the provider credentials stored in your integration
* Regenerating a webhook subscription's signing secret — and reading it, which is redacted for everyone else
* Inviting a member, removing a member, and setting a member's permissions
* Creating an API key
* Transferring ownership of the workspace
## Permissions That Reach Further Than Their Name
Read these before granting them to a key you do not fully control.
* **Contacts write deletes and overwrites in bulk.** It covers `/api/contacts.delete`, and it covers `/api/contacts.import`, which overwrites every field each row carries — a single call can rewrite your whole contact database. It is not "can add contacts". Contacts read is likewise not confined to profiles: it covers the contact timeline and custom events.
* **Lists write is an implicit Contacts write.** Subscribing an address through `/api/lists.subscribe` creates or overwrites that contact's whole record, with every field the request carries, **without needing Contacts write** — so a key holding only Lists write can still rewrite contact data. The same call can subscribe someone to a list that is not public. Grant Lists write only where you would grant Contacts write.
* **Broadcasts write sends.** The same switch that lets someone draft a campaign lets them push it to your entire audience — through `/api/broadcasts.schedule`, and again by triggering or resetting the underlying send task.
* **Automations write reads contacts, changes lists and sends mail.** A webhook node posts the complete contact record to any URL the automation names, and nothing checks permissions while an automation runs; its other nodes add and remove contacts from lists and send email. So this switch reaches contact data, list membership and sending with no Contacts, Lists or Transactional grant at all.
* **Automations read exposes secrets.** The webhook node configuration it returns includes the bearer tokens configured on those nodes.
* **Blog write runs JavaScript on your domain.** It includes publishing a theme, and a theme carries its own `scripts.js`, served to every visitor of your blog's custom domain. It also switches the blog feature on and off.
* **Templates covers reusable blocks.** Editing one block changes every template that embeds it. Blocks are stored with the workspace's own settings, so their content also comes back from the workspace record.
* **Web Analytics covers annotations.** Annotations have no permission of their own, so this grant is also the right to write on the shared timeline everyone reads. Note that the public tracking collector is governed by nothing at all: it accepts data without authentication, and no permission changes that.
* **Webhook Subscriptions write receives contact and email data.** A subscription chooses the events it carries, so the holder can subscribe to contact and email events and have that data delivered to a URL of their choosing. Read opens the delivery log, which carries the payloads themselves. See [Webhooks](/features/webhooks).
* **Segments read plus Contacts read is a query tool.** `/api/segments.preview` accepts any definition sent to it, not just a saved segment, so the pair can ask counting questions about contact attributes, list membership, custom events and timeline — one count at a time, on any condition. Segments read alone cannot.
* **Message History read is contact data.** The log carries recipient addresses and the data each message was rendered with.
* **LLM write spends.** AI assistant requests go to your configured AI provider on your workspace's billing, and are not capped per key. With a Firecrawl integration configured, it also lets the assistant scrape and search the web through your credentials.
### Transactional Sending and Contacts
**Transactional write on its own is enough to send.** That makes a genuine send-only key practical, and the rules are the same over the API and the [SMTP bridge](/features/transactional-api):
* **Without Contacts write**, the send still creates the recipient contact when it does not exist, but stores nothing beyond the email address, and never modifies an existing contact.
* **Without Contacts read**, a send that sets `cc` or `bcc` is refused — the subject line is rendered against each recipient's whole record. On the SMTP bridge those come from the message's `Cc:` and `Bcc:` headers.
Note that Transactional write sends real mail from three separate endpoints, including `/api/email.testProvider`, which only claims to test a provider.
# AI Assistant
Source: https://docs.notifuse.com/features/ai-assistant
One assistant across the email builder, the blog editor and Web Analytics — writing content, editing templates, and answering questions about your traffic.
Notifuse ships an AI assistant that works inside the tools you already use. It writes and edits blog posts, designs email templates block by block, and answers questions about your website traffic — and with [Firecrawl](/integrations/firecrawl) configured, it can search the web and read pages before it writes.
It runs on **your own provider key**. Notifuse does not resell AI credits or route your prompts through a hosted service: your server calls Anthropic, OpenAI or Gemini directly, and that provider bills you.
## Setting it up
1. Go to **Settings** → **Integrations** → **Add Integration**.
2. Pick an LLM provider — [Anthropic](/integrations/anthropic), [OpenAI](/integrations/openai) or [Gemini](/integrations/gemini) — and enter your API key and model.
3. Optionally add [Firecrawl](/integrations/firecrawl) to give the assistant web access.
Only workspace owners can add or edit integrations. API keys are encrypted before they are stored. Until an LLM integration exists, the assistant panel shows a link to the settings page instead of a chat box.
Using the assistant needs the **LLM write** permission. LLM read gates nothing today — chat is the only LLM endpoint and it answers to write — so the console shows it granted and locked. Because every conversation spends your own provider credits, and Firecrawl's tools run from your server on your credentials, treat LLM write as a spending grant. See [Admins & API Keys](/features/admins-api-keys).
## Where you'll find it
| Surface | Where | What it does |
| - | - | - |
| **AI Email Designer** | The email template editor, in visual mode | Builds and edits the email — blocks, layout, styles, subject line and preview text |
| **AI Blog Assistant** | The blog post editor | Writes the article body, plus title, excerpt, SEO and Open Graph metadata |
| **Web Analytics assistant** | Dashboard, Explore and Goals | Answers questions about your traffic, summarises a period, and drives the dashboard |
Each one opens from a button in the editor it belongs to, and each already knows what is on screen — the current email tree, the current post, the current period and filters — so "make the header tighter" or "why did last week drop" need no further context from you.
### Composing email templates
The email assistant works on the same block tree as the visual editor, so anything it produces is editable by hand afterwards.
| It can | Example |
| - | - |
| Build a template from scratch | "Design a product launch email with a hero, three feature columns and a CTA" |
| Edit existing blocks | "Make the button green and full width on mobile" |
| Add, delete, move and reorder blocks | "Move the testimonial above the pricing section" |
| Set the subject line and preview text | "Write three subject lines for this and use the best one" |
| Select a block so you can see it | It highlights what it just changed in the editor |
It also designs for the template's category: a marketing template always ends with a footer carrying the unsubscribe and preferences links, a transactional one gets no unsubscribe link because an API send has no list to unsubscribe from, and a double opt-in one gets its confirmation button.
After it edits the template, the MJML is compiled and any error is reported in the thread — a broken generation is never presented as a success.
The email assistant is available in the visual editor. Switch the template to MJML code mode and the panel hides, since the assistant edits the block tree rather than raw source.
### Composing blog posts
In the [blog post editor](/blog/posts), the assistant writes into the rich-text document itself — headings, lists, quotes, code blocks, links, images and YouTube embeds — rather than dropping a wall of plain text.
| It can | Example |
| - | - |
| Write a full article | "Write a 1,200-word post about email deliverability for SaaS" |
| Rework what's there | "Tighten the intro and add a summary table" |
| Fill in metadata | "Write the excerpt and the SEO title and description" |
| Illustrate the post | With Firecrawl, it searches for real image URLs instead of inventing placeholders |
### Querying web analytics
The [Web Analytics assistant](/web-analytics/ai-assistant) answers questions about your own traffic by running the same queries the dashboard runs, so an answer and the chart next to it cannot disagree. It also changes the period, applies filters and opens Explore reports for you.
It is deliberately fenced: it never groups or filters by visitor email address, never reports per-session coordinates, and does not invent figures. See [the Web Analytics assistant page](/web-analytics/ai-assistant) for the full detail.
## Web access with Firecrawl
Add the [Firecrawl integration](/integrations/firecrawl) and two tools become available to the assistant everywhere it appears:
| Tool | What it does |
| - | - |
| `search_web` | Searches the web and returns relevant URLs with titles and snippets |
| `scrape_url` | Fetches a page and returns its main content as clean markdown |
That is what turns "write a post about X" into a researched post, and lets you say "read our pricing page and write the launch email from it". Each call runs on your Notifuse server and appears as a step in the conversation, so you can see what it read.
Every search and scrape counts against your Firecrawl quota, on top of the tokens the model spends reading the result.
## Choosing a provider
| Provider | Model | Notes |
| - | - | - |
| **[Anthropic](/integrations/anthropic)** | Picked from a list of Claude models | The default choice for template and content generation |
| **[OpenAI](/integrations/openai)** | Any model name you type | A **Base URL** field makes it work with any OpenAI-compatible API, and a **Reasoning effort** setting controls how much reasoning models think |
| **[Gemini](/integrations/gemini)** | A Gemini model, or any model ID you type | |
Because the OpenAI integration accepts a Base URL, it also covers [OpenRouter](https://openrouter.ai) — one key for hundreds of models from Google, Meta, Mistral, DeepSeek, Qwen and others — as well as Azure OpenAI and self-hosted runtimes such as Ollama, vLLM and LiteLLM. Point **Base URL** at `https://openrouter.ai/api/v1`, paste your `sk-or-v1-...` key, and type the model ID you want.
Configure more than one LLM integration and a selector appears at the bottom of the assistant panel, so you can switch model mid-task — a cheap one to draft, a stronger one to fix the layout.
The assistant works by calling tools with structured arguments: MJML blocks, rich-text JSON, analytics queries. Capable models handle this well; small or heavily quantised models tend to produce malformed structures, call the wrong schema, or give up after a failed call. If results come back broken or vague, try a stronger model before assuming the feature is at fault.
## Cost
For Anthropic and Gemini, the running cost of the conversation is shown at the bottom of the panel, split into input and output, and it resets when you start a new conversation. It is computed from the token counts your provider returns and the published price of the model you picked.
The OpenAI integration accepts any model ID — including everything on OpenRouter and any self-hosted runtime — so there is no price list to compute against, and no cost is shown. Track that spend in your provider's dashboard.
Costs land on your provider account, not on Notifuse. Tool use makes a conversation more expensive than a plain chat of the same length: each step is another request, and a scraped page or a query result is more input tokens to read.
## Good to know
| | |
| - | - |
| **Conversations are not saved** | The thread lives in your browser while the editor is open. **New conversation** clears it, and so does closing the editor. |
| **Nothing is applied behind your back** | The assistant edits what is on screen; the post or template is saved when you save it, as usual. |
| **One message, a bounded number of steps** | A single request runs a limited chain of tool calls before it must answer. Ask for a big job in a few messages rather than one. |
| **Your data goes to your provider** | Whatever the assistant reads to answer — the email tree, the post, analytics rows, a scraped page — is sent to the model you configured. Choose the provider accordingly. |
## Related
* [Anthropic](/integrations/anthropic), [OpenAI](/integrations/openai), [Gemini](/integrations/gemini) — provider setup, models and pricing
* [Firecrawl](/integrations/firecrawl) — web search and scraping
* [Templates](/features/templates) — the email editor the assistant works in
* [Blog posts](/blog/posts) — the post editor the assistant writes into
* [Web Analytics assistant](/web-analytics/ai-assistant) — the analytics-specific behaviour and its limits
# Audit logs
Source: https://docs.notifuse.com/features/audit-logs
Who did what in a workspace: members, permissions, API keys, integrations, settings and content changes, with the address it came from and whether it was allowed. Append-only, exportable, with a retention you control.
## Overview
The audit log records every control-plane action taken on a deployment: a member invited, a permission set changed, an API key created, an integration edited, a template saved, a broadcast scheduled, a contact deleted, the licence installed. Each entry names the actor, the action, what it was done to, the IP address and user agent it came from, the request id, and whether it succeeded, failed or was refused. The security-sensitive actions also carry a before/after of what changed, with every secret redacted.
It never records the data plane — contact upserts, sends, tracking, custom events — which can run at thousands of requests a minute and belongs in [Logs](/features/logs), not in a trail.
Audit logs are a **licensed capability** (Enterprise): recording happens only while a licence key covering `audit_logs` is installed. Reading, exporting and configuring the retention are never gated, so the page works in every licence state on whatever was recorded. See [Licence keys](/self-hosting/licence).
**Nothing is refused, nothing is hidden.** An unlicensed deployment writes no entries and is told nothing; a deployment whose key lapses stops recording after the 30-day grace period and writes one marker entry, `licence.recordingStopped`, so the gap is never silent. Installing a key again writes `licence.recordingResumed`.
## Where to find it
**Logs → Audit logs** in a workspace shows that workspace's entries, next to the message and webhook logs. A root user (a `ROOT_EMAIL`) can switch the same tab to **Whole deployment**: every workspace plus the deployment-level entries that belong to no workspace — sign-ins, the licence, the system settings.
Owners always see the tab. A member or an API key sees it only when granted the **Audit logs** read permission, which is an opt-in grant: it is never part of "full access", because full access should not imply reading every colleague's email address, IP address and refused actions.
**Settings → Audit logs** is where an owner sets the workspace's retention, and where the licence notice appears when nothing is being recorded.
## What an entry contains
| Field | Meaning |
| - | - |
| `occurred_at` | When the action completed, UTC. |
| `action` | The RPC route that performed it, verbatim: `workspaces.inviteMember`, `templates.update`, `contacts.delete`. |
| `category` | A grouping for filtering: `auth`, `licence`, `system`, `workspaces`, `members`, `api_keys`, `integrations`, `webhooks`, `templates`, `broadcasts`, `automations`, `lists`, `segments`, `transactional`, `blog`, `contacts`, `audit`. |
| `outcome` | `success`, `failure` (the request was refused for another reason, or a login failed) or `denied` (403 or 402). |
| `status_code` | The HTTP status the request answered with. |
| `actor_type`, `actor_id`, `actor_email`, `actor_name`, `actor_role` | Who did it: a `user`, an `api_key`, the `system` (retention purges, licence markers) or `anonymous` (a failed sign-in). The role is `owner`, `member` or `root`. |
| `auth_method` | `session`, `api_key`, `magic_code`, `sso` or `root_password`. |
| `target_type`, `target_id`, `target_name` | What it was done to. |
| `ip_address`, `user_agent`, `request_id` | Where it came from. The request id is echoed on every API response as `X-Request-ID`, so an entry can be tied back to an application log line. |
| `changes` | Before/after of the fields that changed, as `{ "field": { "old": …, "new": … } }`. Passwords, secrets, tokens and keys are replaced by `[redacted]` on both sides; the fact that they changed is kept. |
| `metadata` | Free-form detail: the permission scope granted, the number of contacts imported, the reason a sign-in failed. |
## What is recorded
Every `POST /api/…` route below is recorded under its own name. Reads are not recorded, with one exception: the console's contact export asks for the first page of `contacts.list` with `export=true`, and that request is recorded as `contacts.export`. An API client paging the list on its own is a read like any other and is not.
| Category | Actions |
| - | - |
| auth | `user.signin`, `user.verify`, `user.rootSignin`, `user.oidc.exchange`, `user.oidc.callback`, `user.logout`, `setup.initialize` |
| licence | `licence.set`, `licence.recordingStopped`, `licence.recordingResumed` |
| system | `settings.update` |
| workspaces | `workspaces.create`, `workspaces.update`, `workspaces.delete`, `workspaces.setBlogSettings`, `workspaces.setCustomFieldLabels`, `workspaces.setWebAnalyticsSettings`, `workspaces.setAuditLogSettings` |
| members | `workspaces.inviteMember`, `workspaces.acceptInvitation`, `workspaces.deleteInvitation`, `workspaces.removeMember`, `workspaces.setUserPermissions` |
| api\_keys | `workspaces.createAPIKey`, `workspaces.connectZapier` |
| integrations | `workspaces.createIntegration`, `workspaces.updateIntegration`, `workspaces.deleteIntegration`, `ses.enableTenantIsolation`, `webhooks.register` |
| webhooks | `webhookSubscriptions.create`, `.update`, `.delete`, `.toggle`, `.regenerateSecret` |
| templates | `templates.create`, `.update`, `.delete`, `templateBlocks.create`, `.update`, `.delete` |
| broadcasts | `broadcasts.create`, `.update`, `.schedule`, `.pause`, `.resume`, `.cancel`, `.delete`, `.retryFailed`, `.selectWinner`, `.sendToIndividual` |
| automations | `automations.create`, `.update`, `.delete`, `.activate`, `.pause` |
| lists, segments | `lists.create`, `.update`, `.delete`, `segments.create`, `.update`, `.delete`, `.rebuild` |
| transactional | `transactional.create`, `.update`, `.delete` |
| blog | `blogPosts.*`, `blogCategories.*`, `blogThemes.*` |
| contacts | `contacts.delete`, `contacts.import`, `contacts.export`, `customEvents.import`, `annotations.*`, `webAnalytics.backfillStart`, `.backfillCancel` |
| audit | `auditLogs.export`, `audit.purged` |
`GET /api/auditLogs.actions` returns this catalogue, which is what the console's action filter is built from.
**Failures are recorded too.** A sign-in for an unknown address, a wrong or expired code, a rate-limited attempt: the response gives nothing away, and the entry says why (`metadata.reason`). A request refused by a permission check is recorded as `denied` with the resource and permission it lacked; one refused by the licence is `denied` with the feature it needed.
**Not recorded**: the data plane (`contacts.upsert`, `customEvents.upsert`, `transactional.send`, `lists.subscribe` and the other per-contact routes), reads, tests and previews, the internal task endpoints, and a broadcast paused automatically by the circuit breaker — that runs on the send path, which contains no licence check of any kind.
## Filtering and export
The page filters by date range, action, outcome, actor email, target id and IP address; the API accepts the same filters plus `categories`, `actor_id`, `actor_type` and `target_type`. Pages are cursor-based, newest first.
**Export** downloads the current filter as CSV or NDJSON, up to 100 000 rows; an export that hits the cap ends with an `export.truncated` marker row carrying `max_rows`, so a truncated file never passes for a complete one — narrow the date range and export again. The export is itself recorded as `auditLogs.export`. Every entry carries an id, and the details drawer has a **Copy link** button that opens the page on that entry.
## Retention
Entries older than the retention are deleted once a day. The default is **365 days**; a workspace owner can set the workspace's own retention in Settings → Audit logs (0 keeps every entry forever, otherwise 30 to 3650 days), and a root user sets the deployment default — which also governs deployment-level entries and the entries of workspaces that no longer exist — in the system settings. Each purge that removed something is recorded as `audit.purged`, with the count.
The retention runs in every licence state: deleting data never depends on the licence.
## Integrity
The table is **append-only**: a database trigger refuses every `UPDATE` and `TRUNCATE`, and every `DELETE` except the ones the retention function performs. No code path in Notifuse rewrites history, and neither does a stray SQL statement run through its database role.
What it does not stop: the owner of the database, or a superuser, who can disable the trigger or drop the table. The guarantee is "the application cannot alter the log", not "nobody can". If you need to prove that to an auditor, back the table up out of band on a schedule they accept.
Entries are stored in the system database, not in the workspace's own database, so deleting a workspace deletes nothing from the log: the record of who deleted it, and of what they did before, survives.
## API
The four endpoints are documented in the API reference: [`auditLogs.list`](/api-reference/list-audit-logs), [`auditLogs.get`](/api-reference/get-an-audit-log-entry), [`auditLogs.actions`](/api-reference/list-audit-actions) and [`auditLogs.export`](/api-reference/export-audit-logs). An API key can call them when granted the Audit logs read permission, which is how a SIEM pulls the log on a schedule. The retention is set over the API with [`workspaces.setAuditLogSettings`](/api-reference/set-the-workspace-audit-log-retention), which only a workspace owner can call — no permission grants it.
# Automations
Source: https://docs.notifuse.com/features/automations
Create automated workflows that trigger based on contact behavior and events
Marketing automations let you create event-driven workflows that automatically engage contacts based on their behavior and lifecycle events. Instead of manually sending emails, automations trigger actions when specific events occur—like a contact subscribing to a list, opening an email, or completing a custom event.
**Key Benefits:**
* **Automated nurturing**: Welcome sequences, onboarding flows, re-engagement campaigns
* **Triggered emails**: Send the right message at the right moment based on contact actions
* **Behavioral responses**: React to email opens, clicks, bounces, and custom events
## Automation Lifecycle
Automations have three states that control their behavior:
| Status | Description |
| - | - |
| **Draft** | Building and testing mode. No contacts are enrolled. |
| **Live** | Active automation. Contacts are enrolled when trigger events occur. |
| **Paused** | Stops new enrollments. In-progress journeys continue to completion. |
**Workflow:**
1. Create automation in **Draft** mode
2. Configure trigger and add nodes
3. **Activate** to go live and start enrolling contacts
4. **Pause** when needed—active contacts continue their journey
5. **Delete** performs soft-delete with recovery capability
## Triggers
Triggers define when contacts enter an automation. They're based on events that appear in the contact timeline.
### Trigger Frequency
| Frequency | Behavior |
| - | - |
| `once` | Contact enters only on the first occurrence of the event |
| `every_time` | Contact can re-enter each time the event occurs |
### Event Types
Automations can trigger on any of these timeline events:
#### Contact Events
| Event | Description |
| - | - |
| `contact.created` | New contact added to workspace |
| `contact.updated` | Contact fields modified |
| `contact.deleted` | Contact removed from workspace |
#### List Events
| Event | Description |
| - | - |
| `list.subscribed` | Contact subscribed to a list |
| `list.unsubscribed` | Contact unsubscribed from a list |
| `list.confirmed` | Contact confirmed subscription (double opt-in) |
| `list.resubscribed` | Previously unsubscribed contact resubscribed |
| `list.bounced` | Email to contact bounced |
| `list.complained` | Contact marked email as spam |
| `list.pending` | Contact in pending confirmation state |
| `list.removed` | Contact removed from list |
List events require selecting a specific list in the trigger configuration.
#### Segment Events
| Event | Description |
| - | - |
| `segment.joined` | Contact matched segment criteria |
| `segment.left` | Contact no longer matches segment criteria |
#### Email Events
| Event | Description |
| - | - |
| `email.sent` | Email was sent to contact |
| `email.delivered` | Email was delivered |
| `email.opened` | Contact opened email |
| `email.clicked` | Contact clicked a link in email |
| `email.bounced` | Email bounced |
| `email.complained` | Contact marked as spam |
| `email.unsubscribed` | Contact unsubscribed via email link |
#### Custom Events
| Event | Description |
| - | - |
| `custom_event` | User-defined event sent via API |
Custom events require specifying the event name (e.g., `purchase`, `trial_started`).
### Field-Specific Triggers
For `contact.updated` events, you can filter by specific fields that changed:
**Core Fields:** `first_name`, `last_name`, `phone`, `external_id`, `timezone`, `language`, `photo_url`
**Address Fields:** `address_line_1`, `address_line_2`, `country`, `state`, `postcode`
**Custom Fields:**
* String: `custom_string_1` through `custom_string_5`
* Number: `custom_number_1` through `custom_number_5`
* Date: `custom_datetime_1` through `custom_datetime_5`
* JSON: `custom_json_1` through `custom_json_5`
### Conditional Triggers
Add segment conditions to filter which contacts enter the automation. For example:
* Trigger on `list.subscribed` but only for contacts where `country = "US"`
* Trigger on `contact.updated` but only when `custom_number_1 > 100`
Set them in the trigger panel under **Entry conditions**, or through the API with the `conditions` field.
They use the same condition builder as segments and the Filter node.
A contact who does not match is never enrolled — which differs from placing a Filter node after the
trigger. With **Once per contact**, a contact enrolled and then sent down a Filter node's "No" path has
used up their single enrollment and cannot enter later; an entry condition leaves it available.
A condition counting timeline activity includes the event that fired the trigger. "Has opened at least
3 emails" on an `email.opened` trigger therefore enrols on the third open, not the fourth. When several
events are recorded in the same operation — a bulk import, for instance — all of them are visible to
each other's conditions.
## Node Types
Nodes are the building blocks of automation workflows. They're divided into action nodes (linear flow) and branching nodes (multi-path).
Every node also takes an optional **description**—a short note of your own, set at the top of the node's configuration panel. It appears under the node's title on the canvas and in Flow Stats, so a sequence of Email and Delay nodes explains its intent without opening each one. It has no effect on execution.
### Action Nodes
| Node | Purpose | Key Configuration |
| - | - | - |
| **Trigger** | Entry point for the automation | `event_kind` (required), `frequency`, `conditions` |
| **Delay** | Pause execution for a duration | `duration` (1-365), `unit` (minutes/hours/days) |
| **Email** | Send a template to the contact | `template_id` (required), `subject_override`, `from_override` |
| **Add to List** | Subscribe contact to a list | `list_id`, `status` (subscribed/pending) |
| **Remove from List** | Unsubscribe contact from a list | `list_id` |
| **Webhook** | Send HTTP POST to external URL | `url` (required), `secret` (optional Bearer token) |
### Branching Nodes
| Node | Purpose | Key Configuration |
| - | - | - |
| **Filter** | Route based on conditions (Yes/No) | `conditions`, paths for matching and non-matching |
| **A/B Test** | Split contacts for testing | 2-4 `variants` with weights (must sum to 100%) |
| **List Status Branch** | Route by list subscription status | `list_id`, 3 paths: not in list, active, non-active |
### Node Details
#### Delay Node
Pauses the contact's journey for a specified duration before continuing to the next node.
* **Duration**: 1 to 365
* **Unit**: `minutes`, `hours`, or `days`
**Example**: Wait 24 hours after signup before sending welcome email.
#### Email Node
Sends an email template to the contact.
* **Template ID**: Required. Select from workspace templates.
* **Subject Override**: Optional. Replace template subject line.
* **From Override**: Optional. Replace sender email/name.
Email nodes require a list to be selected on the automation. This ensures contacts have proper subscription context.
#### Filter Node
Routes contacts based on segment conditions. Contacts matching the conditions continue on the "Yes" path; others take the "No" path or exit.
Uses the same condition builder as segments—supports contact fields, custom events, and complex AND/OR logic.
#### A/B Test Node
Splits contacts into variants for testing different paths.
* **Variants**: 2 to 4 variants (A, B, C, D)
* **Weights**: Each variant has a weight (1-100%), must sum to 100%
* **Deterministic**: Same contact + node always gets the same variant (uses FNV-32a hash)
**Example**: Test two email templates with 50/50 split.
#### List Status Branch Node
Routes contacts based on their subscription status in a specific list:
| Path | Condition |
| - | - |
| **Not in List** | Contact is not subscribed to the list |
| **Active** | Contact has `active` status in the list |
| **Non-Active** | Contact has `pending`, `unsubscribed`, `bounced`, or `complained` status |
#### Webhook Node
Sends contact and automation data to an external URL via HTTP POST.
* **URL**: Required. Must start with `http://` or `https://`
* **Secret**: Optional. Sent as `Authorization: Bearer {secret}` header
The payload includes contact data, automation context, and execution metadata.
## Visual Flow Editor
The visual flow editor provides a drag-and-drop canvas for building automation workflows.
### Building Workflows
1. **Start with Trigger**: Every automation begins with a trigger node
2. **Add Nodes**: Use the node palette or floating "+" buttons on unconnected outputs
3. **Connect Nodes**: Drag connections between nodes
4. **Configure**: Click a node to open its configuration panel
## Exit on Reply
When a contact **replies** to an email an automation sent, you usually want to stop the rest of the sequence—continuing to send scheduled emails to someone who already responded is a poor experience. Enable **Exit on reply** on an automation to handle this automatically.
### Enabling it
Toggle **Exit on reply** in the automation's settings when creating or editing it. Once enabled, a genuine reply to any email that automation sent will exit the contact's journey (recorded with the `replied` exit reason).
**Exit on reply requires inbound email setup at your email provider.** Replies are delivered back to Notifuse by the provider's inbound-parsing feature. This is currently supported for **Mailgun** (see [Mailgun → Inbound replies](/integrations/mailgun#inbound-replies-stop-on-reply)) and **Amazon SES** (see [Amazon SES → Inbound replies](/integrations/amazon-ses#inbound-replies-stop-on-reply))—each needs a one-time setup. On other providers the toggle has no effect until inbound ingestion is configured.
### How replies are matched
A reply is matched to the exact email it responds to using the standard threading headers (`In-Reply-To` / `References`) against the `Message-ID` Notifuse set when it sent the email. Because the match is precise:
* Only the **specific automation** the contact replied to exits. If a contact is enrolled in several automations, replying to one does **not** exit the others.
* Replies that can't be matched to a known send are ignored—no journey is exited.
### What counts as a reply
Not every inbound message is treated as a reply:
| Message type | Effect |
| - | - |
| A genuine human reply | Exits the journey (when **Exit on reply** is enabled) |
| Out-of-office / vacation auto-responder | Recorded on the timeline, but never exits |
| Bounce / delivery-status report | Dropped |
Matched replies appear on the contact timeline as `email.replied` (or `email.auto_reply` for auto-responders).
The stop holds even when the next email is mid-delay or already queued for sending, so a contact who replies won't receive the next scheduled email.
## Contact Journeys
Each contact's progress through an automation is tracked as a "journey" with full execution history.
### Journey States
| Status | Description |
| - | - |
| `active` | Currently progressing through the automation |
| `completed` | Reached a terminal node (finished the flow) |
| `exited` | Left early due to filter, unsubscribe, or deletion |
| `failed` | Error after maximum retries |
### Exit Reasons
When a contact exits early, the reason is recorded:
| Reason | Description |
| - | - |
| `completed` | Normal completion at terminal node |
| `filter_rejected` | Failed filter condition with no alternate path |
| `unsubscribed` | Contact unsubscribed from the automation's list |
| `replied` | Contact replied to one of the automation's emails (requires **Exit on reply**) |
| `automation_paused` | Automation was paused while contact was active |
| `manual` | Manually removed by admin |
### Timeline Events
Automation activity is recorded in the contact timeline:
* `automation.start` — Contact enrolled in automation
* `automation.end` — Contact completed, exited, or failed
## Execution Engine
Automations run on a background scheduler that processes contacts through their workflows.
### How It Works
1. **Polling**: Scheduler runs every 10 seconds
2. **Batching**: Contacts are processed in batches for efficiency
3. **Fair Distribution**: Round-robin across workspaces
### Node Processing
* **Multi-node execution**: Up to 10 nodes processed per tick (prevents runaway loops)
* **Crash recovery**: State is persisted after each node
* **Delay handling**: Delay nodes schedule future execution at `scheduled_at` time
### Retry Logic
When node execution fails:
1. Increment retry count
2. Schedule retry with exponential backoff: 1min, 2min, 4min, 8min...
3. After max retries (default: 3), mark as `failed`
## Statistics
Track automation performance with built-in statistics.
### Automation-Level Stats
| Stat | Description |
| - | - |
| **Enrolled** | Total contacts that entered the automation |
| **Completed** | Successfully finished all nodes |
| **Exited** | Left early (filter, unsubscribe, etc.) |
| **Failed** | Errors after max retries |
### Node-Level Stats
Each node tracks:
* **Entered**: Contacts that reached this node
* **Completed**: Successfully processed
* **Failed**: Encountered errors
* **Skipped**: Bypassed (e.g., wrong branch)
## Permissions
Automations answer to the **Automations** permission: **read** opens automations and their complete node graphs, plus one named contact's journey; **write** creates, edits and deletes them, and activates or pauses them.
**Automations write reaches past its own area.** A webhook node posts the complete contact record
to any URL the automation names, and nothing checks permissions while an automation runs — so this
switch is an indirect contact read that Contacts read cannot restrain. Its other nodes add
contacts to lists, remove them, and send email, with no Lists or Transactional grant at all.
**Automations read exposes secrets.** The node configuration it returns includes each webhook
node's URL and the bearer secret saved on it.
See [Admins & API Keys](/features/admins-api-keys).
## Best Practices
1. **Start simple**: Begin with trigger → delay → email, then add complexity
2. **Use filters wisely**: Segment contacts within flows for targeted messaging
3. **Test before going live**: Use a small test list before activating for production
4. **Monitor statistics**: Review enrolled, completed, and failed counts regularly
5. **Check execution logs**: Debug issues using the contact journey history
6. **Name meaningfully**: Use descriptive automation names for easy identification
7. **Consider frequency**: Choose `once` vs `every_time` based on your use case
# Broadcast campaigns
Source: https://docs.notifuse.com/features/broadcast-campaigns
Broadcast campaigns in Notifuse are mass email marketing initiatives that allow you to send targeted messages to a contact list. They provide powerful features for audience targeting with segment filters, A/B testing, UTM tracking, and flexible scheduling options to deliver the right message to the right people at the right time.
## Audience Targeting
Broadcast campaigns offer flexible audience targeting through a contact list combined with optional dynamic segments, allowing you to reach the right people with precision.
### Select a List
Each broadcast targets exactly one contact list:
* **Single List**: Choose one contact list as your base audience
* **Clear Attribution**: Message history and unsubscribes are tracked per list
* **Simple Model**: No duplicate handling needed since each contact appears once per list
**Example**: Send a product announcement to your "Newsletter Subscribers" list.
### Segment Filters
Apply dynamic segment filters to further refine your broadcast audience based on contact properties and behavior:
* **Property-Based Filtering**: Target contacts by `lifetime_value`, `orders_count`, location, or any custom fields
* **Behavioral Filtering**: Reach contacts based on email engagement (opens, clicks, bounces)
* **Combined Filters**: Use multiple segment conditions with AND/OR logic for precise targeting
* **Real-Time Updates**: Segment membership is evaluated at send time, ensuring accuracy
* **List + Segment**: Combine list selection with segment filters for maximum targeting precision
#### Segment Filter Examples
* **High-Value Customers**: Send to "All Customers" list + segment filter for `lifetime_value >= 1000`
* **Re-engagement**: Target "Newsletter" list + segment for contacts who haven't opened emails in 90 days
* **Geographic Campaigns**: Use "All Contacts" + segment filter for `country = "US"` AND `state = "California"`
* **Purchase Behavior**: Target segments with `orders_count >= 3` to reward loyal customers
**Important**: When both a list and segment filters are applied, only contacts that belong to the selected list AND match the segment criteria will receive the broadcast.
## A/B Testing
Optimize your campaigns by testing different variations with your audience.
### Test Setup
* **Sample Size**: Test variations on a percentage of your total recipients
* **Multiple Variations**: Create different subject lines, content, or templates
* **Control Groups**: Automatically split your test audience evenly
### Winner Selection
#### Manual Selection
* **Review Results**: Analyze performance metrics for each variation
* **Choose Winner**: Manually select the best-performing variation
* **Send to Remaining**: Deploy the winning variation to the rest of your audience
#### Automatic Selection
Configure automatic winner selection based on performance metrics:
**Open Rate Winner**
* **Metric**: Highest email open rate percentage
* **Time Period**: Set a specific duration (e.g., 2 hours, 24 hours)
* **Auto-Deploy**: Automatically send winning variation after the period
**Click Rate Winner**
* **Metric**: Highest click-through rate percentage
* **Time Period**: Set a specific duration for evaluation
* **Auto-Deploy**: Automatically send winning variation after the period
### A/B Testing Example
1. Create campaign with 2 subject line variations
2. Set test sample to 20% of your audience
3. Configure automatic winner selection based on open rate after 4 hours
4. System automatically sends winning variation to remaining 80%
## UTM Tracking
### Automatic UTM Parameters
All external links in your email templates automatically receive UTM parameters for comprehensive tracking:
* **utm\_source**: Identifies the email source
* **utm\_medium**: Set to "email" for all broadcast campaigns
* **utm\_campaign**: Uses your campaign name for tracking
* **utm\_content**: Automatically set to the template ID for content identification
### Example UTM Implementation
**Original Link**: `https://yourwebsite.com/products`
**With UTM Parameters**:
```
https://yourwebsite.com/products?utm_source=notifuse&utm_medium=email&utm_campaign=summer_sale_2024
```
### Analytics Integration
* **Notifuse Web Analytics**: Attribute the visits these links produce, in your own database — the parameters above are what the built-in Email channel rule matches on. See [Web Analytics → Attribution](/web-analytics/attribution).
* **Google Analytics**: Track email performance in your analytics dashboard
* **Campaign Attribution**: Understand which emails drive website traffic and conversions
* **ROI Measurement**: Calculate campaign effectiveness and return on investment
## Data Feeds
Data feeds allow you to fetch external data from your APIs and use it in your email templates. This enables dynamic, personalized content that goes beyond stored contact properties.
### Global Data Feed
The global data feed fetches data once before the broadcast starts and makes it available to all recipients.
#### Configuration
* **Enable Toggle**: Turn on to activate the global data feed
* **Feed URL**: The HTTPS endpoint that returns JSON data
* **Custom Headers**: Add authentication or custom headers (e.g., `Authorization: Bearer token123`)
#### How It Works
1. When the broadcast starts, Notifuse sends a POST request to your configured URL
2. The request body includes broadcast, list, and workspace information:
```json theme={null}
{
"broadcast": {
"id": "abc123",
"name": "Summer Sale"
},
"list": {
"id": "list456",
"name": "Newsletter Subscribers"
},
"workspace": {
"id": "ws789",
"name": "My Company"
}
}
```
3. Your API should return a JSON object with the data you want to use
4. The data is cached and available in templates as the `global_feed` variable
#### Template Usage
Access global feed data in your templates using the `global_feed` variable:
```liquid theme={null}
{% if global_feed.promotion %}
{{ global_feed.promotion.title }}
{{ global_feed.promotion.description }}
Shop Now
{% endif %}
```
#### Use Cases
* **Dynamic Promotions**: Fetch current sale information, discount codes, or featured products
* **Real-time Content**: Display live inventory counts, pricing, or availability
* **Centralized Data**: Pull content from a CMS or marketing platform
### Per-Recipient Data Feed
The per-recipient data feed fetches personalized data for each individual recipient before sending their email.
#### Configuration
* **Enable Toggle**: Turn on to activate the per-recipient data feed
* **Feed URL**: The HTTPS endpoint that returns JSON data (must use HTTPS)
* **Custom Headers**: Add authentication or custom headers
#### How It Works
1. Before sending each email, Notifuse sends a POST request to your configured URL
2. The request body includes contact, broadcast, list, and workspace information:
```json theme={null}
{
"contact": {
"email": "john@example.com",
"external_id": "user_789",
"timezone": "America/New_York",
"language": "en",
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"phone": "+1234567890",
"address_line_1": "123 Main St",
"address_line_2": "Apt 4B",
"country": "US",
"postcode": "10001",
"state": "NY",
"job_title": "Engineer",
"custom_string_1": "value",
"custom_number_1": 42,
"custom_datetime_1": "2024-06-15T00:00:00Z",
"custom_json_1": {"key": "value"},
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-06-01T14:00:00Z"
},
"broadcast": {
"id": "abc123",
"name": "Summer Sale"
},
"list": {
"id": "list456",
"name": "Newsletter Subscribers"
},
"workspace": {
"id": "ws789",
"name": "My Company"
}
}
```
All contact fields are included when populated. Empty fields are omitted. Custom fields (`custom_string_1` through `custom_string_5`, `custom_number_1` through `custom_number_5`, `custom_datetime_1` through `custom_datetime_5`, `custom_json_1` through `custom_json_5`) are included when set on the contact.
3. Your API should return a JSON object with personalized data for that contact
4. The data is available in templates as the `recipient_feed` variable
#### Template Usage
Access per-recipient feed data using the `recipient_feed` variable:
```liquid theme={null}
{% if recipient_feed.recommendations %}
Recommended for You
{% for product in recipient_feed.recommendations %}
{{ product.name }} - ${{ product.price }}
{% endfor %}
{% endif %}
```
#### Use Cases
* **Product Recommendations**: Fetch personalized product suggestions based on purchase history
* **Account Information**: Display current subscription status, points balance, or loyalty tier
* **Dynamic Content**: Show content based on user preferences stored in your system
### Error Handling
#### Automatic Retry
If a per-recipient feed request fails, Notifuse automatically retries:
* **Retry Attempts**: 2 retries before marking as failed
* **Retry Delay**: 5 seconds between attempts
* **Graceful Degradation**: If all retries fail, the broadcast pauses automatically
#### Circuit Breaker
To protect your broadcast from cascading failures:
* **Failure Detection**: If feed requests consistently fail, the broadcast pauses
* **Manual Resume**: After fixing the issue, manually resume the broadcast from the UI
* **Skipped Recipients**: Failed recipients are marked in Message History Logs
### Performance Considerations
#### Global Feed
* **Single Request**: Only one API call per broadcast, regardless of audience size
* **Cached Data**: Data is fetched once and reused for all recipients
* **Recommended**: Use for data that's the same for all recipients
#### Per-Recipient Feed
* **Request Per Recipient**: One API call for each recipient in your audience
* **Processing Impact**: Can significantly slow down broadcast processing for large audiences
* **API Rate Limits**: Ensure your API can handle the request volume
* **Recommended**: Use only when truly personalized data is required
#### Best Practices
* **Fast API Responses**: Keep your feed endpoints fast (under 2 seconds)
* **Reliable Infrastructure**: Ensure high availability for your feed endpoints
* **Fallback Content**: Design templates to gracefully handle missing feed data
* **Test Before Sending**: Use the "Test" button to verify feed responses before launching
### Security
Data feed URLs are fetched by the Notifuse server, so they are protected against [server-side request forgery (SSRF)](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery):
* **Private addresses are blocked**: By default, the server refuses to connect to private, loopback, link-local, and other reserved IP ranges — including the cloud instance metadata endpoint. This applies whether a feed points at such an address directly or via a hostname that resolves to one, and redirects are re-validated to prevent DNS rebinding.
* **Write access required**: Adding, testing, or refreshing a data feed requires the `broadcasts` **write** permission — the same access needed to create a broadcast. Members with read-only access cannot trigger feed requests.
**Self-hosting:** if you intentionally serve data feeds from services on your own internal network, you can allow private addresses by setting `BROADCAST_DATA_FEED_ALLOW_PRIVATE_HOSTS=true`. This disables SSRF protection for data-feed requests, so only enable it in trusted environments. See [Environment Variables](/self-hosting/installation#optional-variables).
## Permissions
Broadcasts answer to the **Broadcasts** permission: **read** opens broadcasts, their configuration and their A/B test results; **write** creates and edits them, manages data feeds, and schedules, sends, pauses, resumes, cancels and deletes them.
Two things about that are easy to get wrong:
* **Broadcasts write sends to your entire audience.** The switch that lets someone draft a campaign is the switch that lets them push it out. There is no separate "may send" grant.
* **Broadcasts read does not include delivery statistics.** Delivered, open, click, bounce and per-link counts come from the message log, so they need **Message History** read. The recipient list needs **Contacts** read. A member holding Broadcasts read alone sees the campaign, not its results.
See [Admins & API Keys](/features/admins-api-keys).
## Scheduling Options
### Manual Sending
* **Immediate Delivery**: Send your campaign right away
* **Review Before Send**: Final review and approval before deployment
* **Real-time Monitoring**: Track delivery and engagement as it happens
### Scheduled Sending
* **Date & Time Selection**: Choose specific delivery date and time
* **Timezone Awareness**: Respects your workspace timezone settings
* **Advance Planning**: Schedule campaigns days or weeks in advance
* **Modification Window**: Edit or cancel scheduled campaigns before send time
### Scheduling Best Practices
* **Optimal Timing**: Schedule for when your audience is most active
* **Time Zone Considerations**: Consider your audience's geographic distribution
* **Avoid Peak Times**: Prevent delivery during holidays or industry-specific busy periods
## Campaign Workflow
1. **Create Campaign**: Set name, description, and goals
2. **Select Audience**: Choose a target contact list and apply optional segment filters
3. **Choose Template**: Select and customize your email template
4. **Configure A/B Testing**: Set up variations and winner selection criteria (optional)
5. **Configure Data Feeds**: Set up global or per-recipient data feeds (optional)
6. **Review Settings**: Confirm audience size, UTM parameters, and schedule
7. **Send or Schedule**: Deploy immediately or schedule for later delivery
8. **Monitor Results**: Track delivery, opens, clicks, and conversions
## Transmission Failure Handling
### Circuit Breaker Protection
Notifuse includes built-in protection against email delivery issues through a circuit breaker mechanism:
* **Failure Threshold**: After 5 consecutive transmission failures, the circuit breaker automatically pauses the broadcast
* **Automatic Pause**: The broadcast status changes to "Paused" with a clear reason indicating the circuit breaker was triggered
* **Immediate Protection**: This prevents further failed attempts and protects your sender reputation
### Failed Message Behavior
When the circuit breaker triggers:
* **Recipients Are Kept**: Everything still queued is paused, not discarded, and keeps its remaining delivery attempts. Only the handful of messages that were actually refused have spent one.
* **Failed Messages**: Those recipients are marked as "failed" in the Message History Logs, with the provider's own explanation in the Error column.
* **Recoverable**: Nothing is lost by the pause itself. Resume sends the paused recipients, and any that were given up on can be retried separately.
### Manual Resume Required
* **Manual Intervention**: Broadcasts paused by the circuit breaker must be manually resumed from the UI
* **Root Cause Analysis**: Before resuming, investigate and resolve the underlying delivery issues
* **Fresh Start**: When resumed, the broadcast continues from where it left off. Recipients whose attempts were all used up are not resumed automatically — use Retry for those.
### Retrying Failed Recipients
A recipient whose delivery attempts were all refused is held for **7 days**, so you can send to them again once the cause is fixed.
* **Where**: A broadcast that finished with failures reads "Completed — N failed" rather than "Complete". The Retry button sits beside it.
* **When**: Only once the broadcast has finished sending. A broadcast still working through its queue, or one that is paused, has to finish or be resumed first.
* **What happens**: The same email is sent again to those recipients. Fix whatever the provider objected to before retrying, or it will fail a second time.
* **Over the API**: `POST /api/broadcasts.retryFailed` with `workspace_id` and `id`, which answers with how many recipients were requeued.
After 7 days the failed recipients are cleared and can no longer be retried from the broadcast.
### What "Complete" Means
A broadcast reads as complete only when its queue is empty and nobody was given up on. The **Sent** figure counts the messages the email provider accepted — a message it refused is counted under **Failed**, never under Sent.
### Common Failure Scenarios
Circuit breaker activation typically occurs due to:
* **Email Provider Issues**: Temporary outages or rate limit exceeded
* **Authentication Problems**: Invalid API keys or credentials
* **Configuration Errors**: Incorrect SMTP settings or provider configuration
* **Reputation Issues**: High bounce rates or spam complaints
### Recovery Steps
1. **Check Message History Logs** to identify the specific failure reasons
2. **Resolve the underlying issue** (fix credentials, contact provider, etc.)
3. **Resume the broadcast** manually from the campaign interface
4. **Monitor closely** to ensure successful delivery continues
## Best Practices
* **List Segmentation**: Use segment filters to target specific subsets of your list
* **A/B Testing**: Always test subject lines and content for better engagement
* **UTM Consistency**: Use consistent campaign naming for better analytics tracking
* **Data Feeds**: Use global feeds for shared data; reserve per-recipient feeds for truly personalized content
* **Schedule Strategically**: Time your sends for maximum audience engagement
* **Monitor Performance**: Review metrics to improve future campaigns
* **Provider Monitoring**: Keep an eye on your email provider status and limits
* **Test Campaigns**: Send test campaigns to verify configuration before large broadcasts
# Contacts
Source: https://docs.notifuse.com/features/contacts
Contacts are the foundation of your email marketing efforts in Notifuse. They represent your subscribers, customers, and prospects, complete with detailed profiles, custom fields, and engagement history.
## Overview
Contacts in Notifuse are comprehensive customer profiles that store subscriber information, preferences, and engagement data. They support flexible data storage through predefined fields and can be associated with multiple mailing lists.
## Contact Fields
| Field | Type | Description |
| - | - | - |
| `email` | String | Contact's email address — the contact's unique identifier (**Required**) |
| `external_id` | String | Optional reference to the contact in your own system. Not unique — see [Contact Identity](#contact-identity-and-upsert-behavior) |
| `first_name` | String | Contact's first name |
| `last_name` | String | Contact's last name |
| `phone` | String | Phone number |
| `timezone` | String | Contact's ISO timezone |
| `language` | String | Preferred language |
| `job_title` | String | Professional title |
| `address_line_1` | String | Primary address |
| `address_line_2` | String | Secondary address (apartment, suite, etc.) |
| `country` | String | Country ISO format |
| `state` | String | State or province |
| `postcode` | String | Postal/ZIP code |
| `custom_string_1` to `custom_string_5` | String | Custom string fields |
| `custom_number_1` to `custom_number_5` | Number | Custom number fields |
| `custom_datetime_1` to `custom_datetime_5` | DateTime | Custom datetime fields |
| `custom_json_1` to `custom_json_5` | JSON | Custom JSON fields |
## Contact Management
### Contact Identity and Upsert Behavior
The **email address is the unique identifier of a contact**: it is the primary key in the database, and every upsert operation (`contacts.upsert`, `contacts.import`, and the contact embedded in `transactional.send`) matches contacts by email address only:
* **New emails**: Automatically creates a new contact
* **Existing emails**: Updates the existing contact with provided fields
* **Null values**: Setting any field to `null` removes that value from the contact
`external_id` is an optional reference to the contact in your own system (for example, your application's user ID). It is stored, searchable, and returned by the API, but it is **not a unique key**:
* It is never used to match contacts during upserts.
* Notifuse does not prevent several contacts from sharing the same `external_id`.
* If several contacts share the same `external_id`, `contacts.getByExternalID` returns only one of them (which one is not defined).
### Changing a Contact's Email Address
Because the email address is the contact's identity, upserting the same `external_id` with a new email address **creates a second contact** — it does not update the email of the existing one. When a user changes their email address in your system:
1. Fetch the old contact with `contacts.getByEmail` to retrieve its profile and its list subscriptions with their statuses.
2. Upsert a contact with the **new** email address, sending the full profile (upserts merge fields, so include every field you want to keep, such as `external_id`, names, and custom fields).
3. Re-create the list subscriptions on the new contact with `lists.subscribe`.
4. Delete the contact with the **old** email address using `contacts.delete`.
Preserve opt-out statuses when re-creating subscriptions. If the old contact had unsubscribed,
bounced, or complained on a list, do **not** re-subscribe the new email address to that list:
the new contact starts with a clean slate, and blindly re-subscribing silently resets an
opt-out — a consent violation (GDPR / CAN-SPAM) that can hurt your sender reputation.
Also keep in mind:
* **Message history and segment memberships** are keyed to the email address. They stay attached to the old email address and are not transferred to the new contact.
* **Active automations**: deleting the old contact drops it from any automation it is currently progressing through; the new contact does not resume its position.
### List Subscriptions
* Contacts can subscribe to one or multiple mailing lists
* Subscription status is managed per list
* List memberships are tracked through the `contact_lists` relationship
## Contact Segmentation Engine
Notifuse includes a powerful real-time contact segmentation engine that allows you to create dynamic audience segments based on contact properties and engagement behavior. Segments automatically update as contact data changes, ensuring your targeting remains accurate and current.
### Building Segments
Segments are built using a flexible condition system that supports:
#### Contact Property Conditions
Filter contacts based on their profile data using standard comparison operators:
* **Numerical fields**: Custom number fields
* **Date fields**: Custom datetime fields
* **String fields**: `first_name`, `last_name`, `country`, custom string fields
* **Comparison operators**: greater than, less than, equal to, not equal to, contains, etc.
#### Goal-Based Conditions
Build RFM (Recency, Frequency, Monetary) segments using [Custom Events & Goals](/features/custom-events):
* **Monetary**: Total spend, average order value
* **Frequency**: Purchase count, event count
* **Recency**: Activity within a timeframe
Example segments: VIP customers (spent \$1000+), repeat buyers (3+ orders), at-risk customers (no purchase in 90 days).
#### Activity-Based Conditions
Target contacts based on their engagement behavior with your emails and your website:
* **New message**: Contact received an email
* **Open email**: Contact opened an email
* **Click email**: Contact clicked a link in an email
* **Bounce email**: Email bounced for this contact
* **Complain email**: Contact marked an email as spam
* **Unsubscribe from list**: Contact unsubscribed from a mailing list
* **View web page**: Contact viewed a page on your website, filterable by path, time on page and scroll depth
* **Visit website**: Contact made a visit, filterable by entry and exit page, pages viewed, duration, channel, campaign, device and country
The two website conditions require [Web Analytics](/web-analytics/setup) and a visitor who has been [identified](/web-analytics/identifying-visitors).
Activity conditions support additional parameters:
* **Frequency**: Number of times the activity occurred
* **Timeframe**: When the activity happened (e.g., in the last 30 days)
### Condition Logic
Segments support both AND/OR logic for combining multiple conditions:
* **ALL conditions**: Contact must match every condition (AND logic)
* **ANY conditions**: Contact needs to match at least one condition (OR logic)
### Use Cases
Common segmentation scenarios include:
* **VIP Customers**: High spenders with multiple purchases (use goal-based conditions)
* **Engagement Recovery**: Re-engage inactive subscribers who haven't opened emails in 90 days
* **At-Risk Customers**: Previously active buyers with no recent purchases
* **Geographic Targeting**: Send region-specific campaigns based on country or state
* **Behavioral Triggers**: Target users who clicked specific product links
### Technical Details
The segmentation engine leverages contact timeline data introduced in version 7.0, which tracks all contact interactions including messages, webhook events, and profile mutations. Activity events include granular `kind` field types (e.g., `email.opened`, `email.clicked`, `web.pageview`) enabling precise behavioral targeting. All timestamps are stored in UTC timezone and can be evaluated in user-specified timezones for date-based conditions.
### Segment Permissions
Segments have their own **Segments** permission: **read** lists segments and opens one, **write** creates, edits, deletes and rebuilds them, and runs, resets or deletes their background build tasks.
Previewing a segment while you build it, and listing the contacts a segment currently matches, additionally require the **Contacts** read permission — both answer questions about your contacts rather than about the segment definition. A member holding Segments read alone can work with segment definitions, but not see who is in them.
Preview accepts any definition sent to it, not just a saved segment. Someone holding Segments read together with Contacts read can therefore ask counting questions about any contact attribute, list membership, custom event or timeline entry, one count at a time.
## Importing Contacts from CSV
Notifuse provides a powerful CSV import feature that streamlines the process of adding multiple contacts to your workspace. The import system is designed to be intuitive and robust, handling large datasets efficiently.
### Auto-Detection and Field Mapping
The CSV importer automatically detects and suggests field mappings based on your CSV column headers. Common column names like "email", "first\_name", "last\_name", and "phone" are automatically matched to their corresponding contact fields. For custom or non-standard column names, you can manually map each CSV column to the appropriate contact field through an intuitive interface.
### Mailing List Assignment
During the import process, you can select one or multiple mailing lists to add your contacts to. This allows you to immediately segment your imported contacts and ensures they're properly organized within your workspace. All imported contacts will be automatically subscribed to the selected lists upon successful import.
### Progress Tracking and Recovery
The import process includes automatic progress saving, which means if your import is interrupted due to network issues or browser closure, you can resume from where you left off. This is particularly valuable when importing large CSV files with thousands of contacts, ensuring no data is lost during the process.
### Import Permissions
Importing needs the **Contacts** write permission. Selecting one or more mailing lists during the import needs **Lists** write as well.
Bear in mind what an import does: every field a row carries overwrites the stored value on the matching contact. A single import can rewrite your whole contact database, so treat Contacts write as that level of access rather than as "can add contacts".
## Permissions
Contact data answers to the **Contacts** permission:
* **Read** covers contact records and their full profiles, the contact timeline, and the custom events recorded against contacts. It is also required — alongside the matching Lists or Segments grant — to enumerate the addresses inside a list or a segment, and to set `cc` or `bcc` on a transactional send.
* **Write** covers creating and updating contacts, **deleting** them, **bulk-overwriting** them through a CSV or API import, and recording custom events. Changing a contact's subscription status on a list needs Lists write as well.
Write is not "can add contacts": it deletes permanently, and it overwrites in bulk.
Three grants reach contact data without Contacts write, which is worth knowing before you hand any of them out:
* **Lists write** on its own creates or overwrites a complete contact record. Subscribing an address through `/api/lists.subscribe` writes every field the request carries, and can subscribe someone to a list that is not public.
* **Automations write** sends the complete contact record to any URL a webhook node names, and nothing checks permissions while an automation runs.
* **Transactional write** creates the recipient contact as part of sending. Without Contacts write, only the email address is stored and an existing contact is never modified — see [Transactional API](/features/transactional-api#permissions).
## Key Features
* **Flexible Schema**: Predefined fields cover most use cases while custom fields provide extensibility
* **Automatic Deduplication**: Email-based upserts prevent duplicate contacts for the same email address
* **Multi-List Support**: Single contact can belong to multiple mailing lists
* **Data Removal**: Explicit null handling for field cleanup
# Custom Events & Goals
Source: https://docs.notifuse.com/features/custom-events
Track user behavior and conversion goals to build RFM segments and enrich contact timelines.
## Overview
Custom events track user actions from external systems (e-commerce, CRM, analytics). They appear in the contact timeline and enable goal-based segmentation for building RFM (Recency, Frequency, Monetary) segments.
## Contact Timeline
Every custom event creates an entry in the contact timeline, giving you a complete history of user interactions:
Events display with their `event_name` as the timeline entry type (e.g., "order.completed", "subscription.started"). The `properties` field stores additional context visible in the timeline details.
## Goal Types
Goals are the key to building powerful segments. Each event can optionally track a goal:
| Type | Use Case | goal\_value |
| - | - | - |
| `purchase` | E-commerce orders, one-time payments | Required |
| `subscription` | SaaS subscriptions, memberships | Required |
| `lead` | Contact forms, demo requests | Optional |
| `signup` | Account registrations | Optional |
| `booking` | Appointments, consultations | Optional |
| `trial` | Free trial activations | Optional |
| `other` | Custom conversion events | Optional |
## Building RFM Segments
RFM (Recency, Frequency, Monetary) analysis identifies your best customers. Use the segment builder with "Custom Events Goals" conditions:
### Monetary: Total Spend
Select goal type "purchase", aggregate "sum", and set a minimum value to find high-spending customers.
### Frequency: Purchase Count
Select goal type "purchase", aggregate "count", and set a minimum number to find repeat buyers.
### Recency: Recent Activity
Use the timeframe filter "in the last X days" to find customers with recent purchases.
### Example RFM Segments
* **VIP Customers**: Spent \$1000+ AND 5+ orders AND purchased in last 60 days
* **At-Risk High-Value**: Spent \$500+ AND no purchase in 90 days
* **New High-Potential**: First purchase over \$200 in last 30 days
### Aggregate Options
| Option | Description |
| - | - |
| Sum | Total of all goal values (lifetime spend) |
| Count | Number of events (order count) |
| Average | Average goal value (AOV) |
| Min | Smallest goal value |
| Max | Largest goal value |
### Timeframe Options
| Option | Description |
| - | - |
| Anytime | All historical data |
| In the last X days | Rolling window |
| In date range | Specific period |
| Before date | Historical cutoff |
| After date | Since a specific date |
## Tracking Events
### Single Event
```bash theme={null}
curl -X POST "https://your-instance.com/api/customEvents.upsert" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "your_workspace",
"email": "customer@example.com",
"event_name": "order.completed",
"external_id": "order_12345",
"properties": {
"items": ["Product A", "Product B"],
"currency": "USD"
},
"goal_type": "purchase",
"goal_value": 149.99
}'
```
### Batch Import
Import up to 50 events at once:
```bash theme={null}
curl -X POST "https://your-instance.com/api/customEvents.import" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "your_workspace",
"events": [
{
"email": "user1@example.com",
"event_name": "order.completed",
"external_id": "order_001",
"goal_type": "purchase",
"goal_value": 99.99
},
{
"email": "user2@example.com",
"event_name": "order.completed",
"external_id": "order_002",
"goal_type": "purchase",
"goal_value": 249.99
}
]
}'
```
## Event Name Format
Event names must be lowercase using letters, numbers, dots, underscores, hyphens, or slashes:
* `order.completed`
* `subscription/started`
* `form_submit`
* `user-signup`
Goals recorded by [Web Analytics](/web-analytics/goals) reach the contact timeline through a narrower rule: ASCII letters, numbers, dots and slashes only, with everything else — hyphens and accented characters included — replaced by an underscore. A web goal named `user-signup` arrives as `user_signup`. If you send the same event through both paths, pick a name that survives each.
## Handling Refunds
Use negative `goal_value` for refunds to keep accurate LTV calculations:
```json theme={null}
{
"email": "customer@example.com",
"event_name": "order.refunded",
"external_id": "refund_12345",
"goal_type": "purchase",
"goal_value": -50.00
}
```
## Permissions
Custom events have no permission of their own: they answer to **Contacts**. Recording or importing an event needs Contacts **write**, and reading one needs Contacts **read** — so granting Contacts read also opens the events recorded against your contacts, and the contact timeline they feed. See [Admins & API Keys](/features/admins-api-keys).
## Notes
* Contacts are auto-created if they don't exist
* Events update only if `occurred_at` is newer than existing
* Soft-delete by setting `deleted_at` (set to `null` to restore)
* See [API Reference](/api-reference/import-custom-events) for complete endpoint documentation
# File Manager
Source: https://docs.notifuse.com/features/file-manager
The file manager connects to your S3-compatible bucket to store and serve files and images used in your email templates. You can optionally configure a CDN endpoint to customize S3 URLs for better performance.
## S3 Storage Configuration
Configure your S3-compatible storage with these settings:
* **S3 Endpoint**: Your S3-compatible storage endpoint URL
* **S3 Access Key**: Access key for authentication
* **S3 Secret Key**: Secret key for authentication
* **S3 Bucket**: Bucket name where files will be stored
* **S3 Region**: Storage region (e.g., us-east-1)
## CDN Configuration
### CDN Endpoint
* **Purpose**: URL of the CDN that caches your files
* **Benefit**: Faster file delivery and reduced bandwidth costs
* **Optional**: CDN configuration is not required but recommended
### URL Customization
When a CDN endpoint is configured, file URLs in your email templates will use the CDN URL instead of the direct S3 URL for optimized delivery.
## CORS Configuration
To allow the file manager to upload files directly from your browser, you need to configure CORS (Cross-Origin Resource Sharing) on your storage bucket.
### Cloudflare R2 / AWS S3
Create a CORS policy with the following configuration:
```json theme={null}
[
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET", "POST", "HEAD", "DELETE", "PUT"],
"AllowedHeaders": ["*"]
}
]
```
For production, replace `"*"` in `AllowedOrigins` with your Notifuse console URL for better security.
### Google Cloud Storage
For GCS, use the `gsutil` command with a JSON configuration file:
```json theme={null}
[
{
"origin": ["*"],
"method": ["GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS"],
"responseHeader": ["Content-Type", "*"]
}
]
```
Apply the CORS configuration:
```bash theme={null}
gsutil cors set cors.json gs://your-bucket-name
```
## Supported Storage Providers
Files can be uploaded to any S3-compatible storage service:
* Amazon S3
* Cloudflare R2
* DigitalOcean Spaces
* Wasabi
* MinIO
* Backblaze B2
* Google Cloud Storage (S3 API)
## Self-Hosted S3 Option
If you prefer not to use cloud storage providers, you can run your own S3-compatible storage server using our self-hosted solution:
A lightweight, Docker-based S3-compatible storage server perfect for self-hosting Notifuse without external dependencies.
This option is ideal for:
* Air-gapped or offline environments
* Development and testing
* Users who want full control over their data
* Avoiding cloud provider costs
## File Usage
Once configured, uploaded files can be used in:
* Email template images
* Attachments
* Brand assets (logos, headers)
* Any media content in your email campaigns
## Template Editor Integration
The visual email editor provides seamless access to your file manager:
* **Modal Access**: File manager opens in a modal while creating templates
* **Direct Integration**: Select and insert files directly from the editor
* **Real-Time Upload**: Upload new files without leaving the template editor
# Lists
Source: https://docs.notifuse.com/features/lists
Lists in Notifuse are organized collections of contacts that allow you to segment your audience and send targeted email campaigns. They provide the foundation for effective email marketing by enabling precise audience targeting and personalized messaging.
Example:
```
- Newsletter Subscribers
- VIP Customers
- Event Attendees
- Product Updates
```
## Contact Statuses
Contacts within lists can have different statuses that determine their engagement eligibility:
| Status | Email Delivery | Description |
| - | - | - |
| **Active** | ✅ Can receive emails | Confirmed subscribers eligible for broadcasts |
| **Pending** | ❌ Cannot receive emails | Waiting for double opt-in confirmation |
| **Unsubscribed** | ❌ Cannot receive emails | Opted out of receiving communications |
| **Bounced** | ❌ Cannot receive emails | Email address is invalid or unreachable |
| **Complained** | ❌ Cannot receive emails | Marked emails as spam |
> **Important**: Only contacts with **"active"** status can receive emails from broadcast campaigns.
## List Visibility
### Public Lists
* **Notification Center**: ✅ Appear in the notification center
* **Subscriber Access**: Users can see and subscribe to these lists through your notification center
* **Use Case**: Ideal for newsletters, announcements, and general communications
### Private Lists
* **Notification Center**: ❌ Do not appear in the notification center
* **Subscriber Access**: Only accessible through direct management or API
* **Use Case**: Perfect for internal communications, VIP segments, transactional lists, or one-time broadcasts
## Double Opt-In Configuration
### Enabling Double Opt-In
When you activate double opt-in for a list:
* **Imported Contacts**: Contacts added via CSV import or API will always be **"active"** (they bypass double opt-in)
* **Anonymous Subscribers**: Users subscribing via forms (like a newsletter signup on your website) will be **"pending"**
* **Confirmation Required**: Pending contacts must click a confirmation link in their email to become "active"
* **Compliance**: Helps ensure GDPR compliance and reduces spam complaints
**Example**: A visitor fills out a newsletter form on your website → they receive a confirmation email → they click the link (which renders the notification center) → status changes from "pending" to "active"
### Welcome Template (Optional)
* **Purpose**: Automatically sent to contacts when they become "active"
* **Timing**: Triggered after successful subscription or double opt-in confirmation
* **Content**: Welcome message, onboarding information, or subscription confirmation
### Unsubscribe Template (Optional)
* **Purpose**: Automatically sent when contacts unsubscribe from the list
* **Timing**: Triggered immediately when a contact's status changes to "unsubscribed"
* **Content**: Confirmation of unsubscription, feedback request, or alternative communication options
## Re-subscription Handling
When a contact attempts to subscribe to a list they have previously interacted with, Notifuse handles the request based on their current status:
| Current Status | Behavior |
| - | - |
| **Active** | No-op. Contact is already subscribed. Returns success silently. |
| **Pending** | No status change. Re-sends the double opt-in confirmation email. |
| **Unsubscribed** | Allowed. Status is set to **"pending"** and a confirmation email is sent, even if the list uses single opt-in. |
| **Bounced** | Blocked. Status is not changed and no email is sent. |
| **Complained** | Blocked. Status is not changed and no email is sent. |
### Why bounced and complained contacts are blocked
Re-sending emails to addresses that have previously hard-bounced or filed spam complaints damages sender reputation and can trigger domain blacklisting. These contacts are permanently suppressed at both the application and database level.
### Bulk import behavior
When contacts are imported via CSV or the API with `subscribe_to_lists`, the same protections apply at the database level. Contacts with **unsubscribed**, **bounced**, or **complained** status will not have their list status overwritten. This prevents accidental reactivation of opted-out contacts through bulk operations.
### Privacy by design
Public subscribe endpoints always return HTTP 200 regardless of the contact's internal status. This prevents email enumeration attacks where an attacker could probe the endpoint to determine whether a specific email address is subscribed.
## Permissions
Lists answer to the **Lists** permission: **read** shows lists, their settings and their subscriber counts, and **write** creates, edits and deletes them and manages subscriptions. Listing the actual member addresses of a list needs **Contacts** read as well, and changing or removing an existing subscription needs **Contacts** write as well.
**Lists write is an implicit Contacts write.** Subscribing an address through `/api/lists.subscribe`
creates or overwrites that contact's whole record, with every field the request carries, without
needing Contacts write — so a member or API key holding only Lists write can still rewrite your
contact data. An authenticated call can also subscribe someone to a list that is not public. Grant
Lists write only where you would grant Contacts write. See
[Admins & API Keys](/features/admins-api-keys).
## Best Practices
1. **Status Management**: Regularly review contact statuses to maintain list health
2. **Double Opt-In**: Enable for compliance and to ensure engaged subscribers
3. **Template Setup**: Configure welcome and unsubscribe templates for better user experience
4. **List Organization**: Use separate lists for different audience types (e.g., "Newsletter", "Product Updates") and combine with segment filters for precise targeting in broadcasts
# Logs
Source: https://docs.notifuse.com/features/logs
Real-time logs for monitoring email delivery and debugging email provider issues. Track message events, webhook data and the workspace audit trail with advanced filtering capabilities.
## Overview
Logs provide real-time monitoring of email activity and webhook events to help debug email delivery issues and track campaign performance. The same section also holds the workspace's **audit trail** — who did what, from where — on its own tab.
## Log Types
### Messages Tab
* **Email Events**: Track delivery, opens, clicks, bounces, and complaints
* **Real-Time Updates**: Events appear immediately as they occur
* **Email Provider Data**: Direct feedback from your configured email providers
* **Error**: When a send was refused, the provider's own reply — the SMTP code and the text that came with it, such as `452: 4.3.1 Daily sending quota exceeded`. Hover to read the whole of it, or hide the column from the column picker.
### Email Provider Webhooks Tab
* **Raw Payloads**: Stores the complete, unmodified webhook data from email providers
* **Real-Time Processing**: Webhooks processed immediately upon receipt
* **Provider Integration**: Data from Mailgun, Amazon SES, Postmark, etc.
### Audit Logs Tab
* **Who Did What**: Every control-plane action — a member invited, a permission changed, an API key created, an integration edited, a template saved, a broadcast scheduled — with the actor, the IP address, the outcome and a before/after of what changed
* **Append-Only**: A database trigger refuses every rewrite; the retention purge is the only deletion
* **Exportable**: Download the current filter as CSV or NDJSON, or pull it over the API into a SIEM
Recording is an Enterprise [licence key](/self-hosting/licence) capability; the tab, its export and its retention setting work in every licence state. See [Audit logs](/features/audit-logs) for the full reference.
## Event Updates
Email events are updated in real-time as email provider webhooks are received:
* **Delivered**: Email successfully delivered to recipient
* **Opened**: Recipient opened the email
* **Clicked**: Recipient clicked a link in the email
* **Bounced**: Email delivery failed
* **Complained**: Recipient marked email as spam
## Filtering Options
Filter logs by multiple criteria:
* **Status**: Filter by delivery status (delivered, bounced, etc.)
* **Delivery**: Show only the messages that failed to send, or only those that did not
* **Email**: Search by specific recipient email address
* **Template**: Filter by email template used
* **Broadcast**: Filter by specific broadcast campaign
## Permissions
The tabs answer to different permissions, all read-only in practice:
* **Messages** needs **Message History** read. This is also where broadcast delivery statistics live — delivered, open, click, bounce and per-link counts — so a member who may create and send a broadcast still cannot see how it performed without it. The log carries recipient addresses and the data each message was rendered with, so treat it as contact data.
* **Email Provider Webhooks** needs **Webhook Events** read. These are the providers' own payloads, including recipient addresses and provider diagnostics.
* **Audit logs** needs **Audit logs** read, which an owner grants deliberately — it is never part of full access, because full access should not imply reading every colleague's email address, IP address and refused actions. Owners always see the tab, and a root user can switch it to the whole deployment.
The write verb of these permissions gates nothing today: these rows are written by the sending pipeline, by the provider callback routes and by the platform's own audit recorder, never by anything you can call. See [Admins & API Keys](/features/admins-api-keys).
## Debugging Email Issues
Logs are essential for troubleshooting email provider problems:
* **Delivery Failures**: Identify bounced emails and reasons, and read why a send was refused in the Error column
* **Provider Issues**: Spot patterns in webhook failures
* **Performance Monitoring**: Track open and click rates
* **Real-Time Debugging**: Immediate feedback on email sending issues
# Notification Center
Source: https://docs.notifuse.com/features/notification-center
The Notification Center is a mobile-first interface that Notifuse automatically renders when users click on unsubscribe links in newsletters. It provides a seamless experience for contacts to manage their email subscription preferences across all public lists.
Example:
## Features
### Automatic Rendering
* Displays automatically when users click unsubscribe links
* Mobile-responsive design
* No configuration required
### Multi-Language Support
Automatically detects browser language and displays content accordingly. Supported languages:
* English (en)
* French (fr)
* Spanish (es)
* German (de)
* Chinese (zh)
* Hindi (hi)
* Arabic (ar)
* Portuguese (pt)
* Russian (ru)
* Japanese (ja)
Defaults to English if browser language is not supported.
### Subscription Management
Users can:
* View all public lists
* Subscribe/unsubscribe with one click
* See real-time feedback messages
## Configuration
### Branding
The notification center uses:
* Logo from workspace settings
* Website URL from workspace settings
## Technical Details
### User Flow
1. User clicks unsubscribe link
2. Notification center opens
3. User modifies subscription preferences
4. Changes applied immediately
### Implementation
* Cross-browser compatible
* Responsive design
* Secure subscription handling
* Error handling with localized messages
# Templates
Source: https://docs.notifuse.com/features/templates
Templates in Notifuse are the foundation of your email design system. They provide reusable, responsive email layouts that can be personalized with dynamic content using Liquid templating and built with MJML components for perfect cross-client compatibility.
## Overview
Email templates in Notifuse are built using a **visual drag-and-drop editor** that combines:
* **MJML Framework**: Guarantees full email client compatibility across all devices and email providers
* **Liquid Templating**: Dynamic content personalization using variables like `{{ contact.first_name }}`
* **Version Control**: Every template update creates a new version to track previously sent contents
* **Multi-Purpose**: Used by both broadcast campaigns and transactional notifications
## Template Builder Features
* **Drag-and-Drop Interface**: Visual editor for easy template creation
* **Responsive Design**: Mobile-optimized layouts that work across all devices
* **Pre-built Blocks**: Headers, footers, buttons, images, and content sections
* **Brand Consistency**: Colors, fonts, logos, and styling options
* **Real-time Preview**: See exactly how your email will look before sending
## Usage
Templates are used across Notifuse for:
* **Broadcast Campaigns**: Mass email campaigns sent to your contact lists
* **Transactional Notifications**: Automated emails triggered by user actions
* **Double Opt-In Emails**: Sent when anonymous contacts subscribe to lists
* **Welcome Emails**: Sent when contacts confirm their subsription to lists
* **Unsubscribe Confirmations**: Sent when contacts opt out
## Translations
A template can carry one version of its content per language. Which version a contact receives is decided at send time from the contact's `language` field, so one broadcast or one transactional notification serves every language you have written — nothing to branch on in your automations or your API calls.
### Enabling languages in the workspace
Translations are only offered for languages the workspace has switched on. Open **Settings → General**:
* **Supported Languages** — the languages this workspace works in. Add every language you intend to write templates for; a language that is not listed here cannot be given a translation.
* **Default Language** — the language a template's main content is written in, and the version a contact receives when nothing better matches. Every workspace has exactly one.
Language codes are two-letter ISO codes such as `en`, `fr`, `de`, `es`. The default language must be one of the supported languages, and the list must contain at least one.
With a single supported language the template editor shows no translation controls at all. The **Translations** tab appears as soon as the workspace supports more than one language.
### Writing a translation
When creating or editing a template, the editor has a tab per stage: **Template**, in the workspace's default language, and **Translations**, listing each other supported language. Open a language to write its version: its own subject, preview text and body, with the same visual editor, MJML code mode and Liquid variables as the default. **Reset translation** copies the default-language content into the translation as a starting point, replacing whatever was there.
A language that has no translation is simply skipped at send time — you do not have to translate a template into every supported language at once.
### Which version a contact receives
At send time Notifuse reads the contact's `language`:
1. If it is empty, or equals the workspace's default language, the contact receives the **default content**.
2. If the template has a translation for that language, the contact receives **that translation**.
3. Otherwise the contact receives the **default content**.
So the default content is always the fallback, and a translation is only ever an improvement over it, never a gap. Set a contact's language on import, through the API (`language` on `contacts.upsert`), or from a subscription form; see [Contacts](/features/contacts).
To check a specific language before sending, the template preview and the test-send dialog both have a **Language** selector.
**Self-hosted: adding a language to a template, or editing the content of an existing translation, needs a [licence key](/self-hosting/licence).** Translations already saved keep being sent in every licence state, editing the rest of a template that carries them is never refused, and removing a translation is always allowed. Every Notifuse Cloud plan includes it.
## Liquid Syntax
Notifuse uses Liquid templating to insert dynamic content into your emails. Variables are enclosed in double curly braces and automatically replaced with actual data when emails are sent.
### Contact Variables
The `contact` object contains the **full contact profile** with all stored information. Access any contact field for personalization:
```liquid theme={null}
Hello {{ contact.first_name }}!
Your account email: {{ contact.email }}
Full name: {{ contact.first_name }} {{ contact.last_name }}
```
**Output Example:**
```
Hello John!
Your account email: john.doe@example.com
Full name: John Doe
```
### List Variables
Reference the mailing list information:
```liquid theme={null}
You're subscribed to: {{ list.name }}
List ID: {{ list.id }}
```
**Output Example:**
```
You're subscribed to: Newsletter
List ID: newsletter
```
### System URLs
Use pre-generated URLs for common actions:
#### Unsubscribe Link
```liquid theme={null}
Unsubscribe from this list
```
#### Subscription Confirmation
```liquid theme={null}
Confirm your subscription
```
#### Notification Center
```liquid theme={null}
Manage your preferences
```
### Complete Example
```liquid theme={null}
Welcome {{ contact.first_name }}!
Thank you for subscribing to {{ list.name }}. We're excited to have you on board!
Your registered email: {{ contact.email }}
Manage Preferences: {{ notification_center_url }}
Don't want to receive these emails?
Unsubscribe here: {{ unsubscribe_url }}
```
### Available Data Structure
When designing templates, you have access to this data structure:
```json theme={null}
{
"contact": {
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com"
},
"list": {
"id": "newsletter",
"name": "Newsletter"
},
"unsubscribe_url": "https://your_notifuse_endpoint.your_website.com/notification-center?action=unsubscribe&email=john.doe@example.com&lid=newsletter&lname=Newsletter&wid=workspace123&mid=msg_123&email_hmac=abc123",
"confirm_subscription_url": "https://your_notifuse_endpoint.your_website.com/notification-center?action=confirm&email=john.doe@example.com&lid=newsletter&lname=Newsletter&wid=workspace123&mid=msg_123&email_hmac=abc123",
"notification_center_url": "https://your_notifuse_endpoint.your_website.com/notification-center?email=john.doe@example.com&email_hmac=abc123&wid=workspace123"
}
```
# Transactional API
Source: https://docs.notifuse.com/features/transactional-api
The Transactional API enables you to send event-driven emails directly from your application. Unlike broadcast campaigns, transactional emails are sent individually to specific contacts in real-time with automatic contact management, making them perfect for user-triggered notifications and automated workflows.
## Common Use Cases
```
- Password Reset
- Order Confirmation
- Welcome Email
- Account Deletion
- Purchase Receipt
- Shipping Notifications
- Account Verification
- Payment Confirmations
```
## Key Features
### Automatic Contact Management
* **Contact Upsert**: Contacts are automatically created or updated before sending
* **Profile Enrichment**: Template data is automatically merged with existing contact profile information
* **Data Consistency**: Ensures contact information is always current
Updating the contact this way requires the Contacts write permission — see [Permissions](#permissions).
### Template Data Enrichment
When you send a transactional email, Notifuse automatically:
1. **Looks up the contact** by email address in your workspace
2. **Merges your API data** with the existing contact profile
3. **Enriches the template** with the complete contact information
4. **Sends the personalized email** with all available data
**Example**: If your API sends `{"order_id": "12345"}` but the contact profile contains `{"first_name": "John", "last_name": "Doe"}`, the template will have access to both the order data and the contact's full profile.
### Deduplication with External ID
* **Prevent Duplicates**: Use `external_id` to prevent sending the same notification multiple times
* **Idempotent Requests**: Notifications with the same `external_id` will only be sent once
* **Custom Identifiers**: Use your own unique identifiers (order IDs, event IDs, etc.)
**Example**: Setting `external_id: "order-12345"` ensures that even if your system sends the same order confirmation multiple times, the email will only be delivered once.
### Email Delivery Options
Configure email routing with flexible options:
* **From Name**: Override the default sender display name
* **Subject**: Override the template subject line (supports Liquid templating)
* **Reply-To**: Set custom reply-to addresses
* **CC**: Add carbon copy recipients
* **BCC**: Include blind carbon copy recipients
* **Attachments**: Attach files to emails (PDFs, images, documents, etc.)
## API Endpoint
Send transactional emails using a simple POST request:
### Request
```bash theme={null}
curl -X POST \
"https://v3.notifuse.com/api/transactional.send" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"workspace_id": "your_workspace",
"notification": {
"id": "password_reset",
"external_id": "pwd-reset-user123-20240101",
"channels": ["email"],
"contact": {
"email": "user@example.com"
},
"data": {
"reset_token": "abc123",
"expiry_time": "2024-01-01T12:00:00Z"
},
"metadata": {
"user_id": "12345",
"trigger": "forgot_password",
"source": "web_app"
},
"email_options": {
"from_name": "Support Team",
"subject": "Reset your password, {{ contact.first_name }}",
"reply_to": "support@yourcompany.com",
"cc": ["admin@yourcompany.com"],
"bcc": ["logs@yourcompany.com"],
"attachments": [
{
"filename": "reset-guide.pdf",
"content": "JVBERi0xLjQKJeLjz9MKMy4uLg==",
"content_type": "application/pdf"
}
]
}
}
}'
```
### Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `workspace_id` | string | Yes | Your workspace identifier |
| `notification.id` | string | Yes | Template identifier for the email |
| `notification.external_id` | string | No | Unique identifier for deduplication |
| `notification.channels` | array | Yes | Must include "email" |
| `notification.contact` | object | Yes | Contact object with at least an email field |
| `notification.data` | object | No | Template variables for email content |
| `notification.metadata` | object | No | Tracking data (not available in templates) |
| `notification.email_options` | object | No | Email routing configuration |
### Email Options
| Parameter | Type | Description |
| - | - | - |
| `from_name` | string | Override default sender display name |
| `subject` | string | Override template subject line (supports Liquid templating, max 255) |
| `reply_to` | string | Set custom reply-to email address |
| `cc` | array | List of carbon copy recipients |
| `bcc` | array | List of blind carbon copy recipients |
| `attachments` | array | File attachments (max 20 files, 3MB per file, 10MB total) |
## Data vs Metadata
### Template Variables (`data`)
The `data` object contains variables that will be available in your email templates:
```json theme={null}
{
"data": {
"order_id": "ORD-12345",
"total": "$99.99",
"discount_code": "WELCOME20"
}
}
```
These can be used in templates as: `{{ order_id }}`, `{{ total }}`, `{{ discount_code }}`
### Tracking Data (`metadata`)
The `metadata` object stores information for analytics and tracking but is **not available** in templates:
```json theme={null}
{
"metadata": {
"campaign_id": "welcome-series",
"source": "mobile_app",
"user_segment": "premium"
}
}
```
Use metadata for internal tracking, analytics, and reporting purposes.
## Email Attachments
Send files with your transactional emails such as invoices, receipts, tickets, or documents. Attachments are automatically deduplicated and stored efficiently.
### Attachment Limits
* **Maximum files**: 20 attachments per email
* **File size**: 3MB per file
* **Total size**: 10MB per email
* **Supported formats**: All file types (PDF, PNG, JPEG, DOCX, etc.)
### Attachment Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `filename` | string | Yes | Name of the file (max 255 characters) |
| `content` | string | Yes | Base64-encoded file content |
| `content_type` | string | No | MIME type (e.g., "application/pdf"), auto-detected if not provided |
| `disposition` | string | No | "attachment" (default) or "inline" |
| `content_id` | string | No | Content-ID for inline attachments, referenced from the HTML body as ``. Only allowed when `disposition` is "inline". Defaults to the filename when omitted. |
### Example with PDF Invoice
```json theme={null}
{
"workspace_id": "your_workspace",
"notification": {
"id": "order_confirmation",
"contact": {
"email": "customer@example.com"
},
"data": {
"order_id": "ORD-12345",
"total": "$99.99"
},
"email_options": {
"attachments": [
{
"filename": "invoice-12345.pdf",
"content": "JVBERi0xLjQKJeLjz9MKMy4uLg==",
"content_type": "application/pdf",
"disposition": "attachment"
}
]
}
}
}
```
### Example with Multiple Attachments
```json theme={null}
{
"email_options": {
"attachments": [
{
"filename": "invoice.pdf",
"content": "JVBERi0xLjQKJeLjz9MK...",
"content_type": "application/pdf"
},
{
"filename": "receipt.png",
"content": "iVBORw0KGgoAAAANSUhEU...",
"content_type": "image/png"
},
{
"filename": "terms.docx",
"content": "UEsDBBQABgAIAAAAIQ...",
"content_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
]
}
}
```
### Inline Images (Content-ID)
Embed per-recipient images — such as QR codes or personalized graphics — directly inside the email body instead of hosting them at a URL. Set `disposition` to `"inline"` and give the attachment a `content_id`, then reference it from your template's HTML as ``:
```json theme={null}
{
"email_options": {
"attachments": [
{
"filename": "check-in-qr.png",
"content": "iVBORw0KGgoAAAANSUhEU...",
"content_type": "image/png",
"disposition": "inline",
"content_id": "checkInQr"
}
]
}
}
```
And in the email template:
```html theme={null}
```
Notes:
* `content_id` may only contain letters, digits, and the characters `.` `_` `-` `@` (max 255 characters), and is only accepted when `disposition` is `"inline"`.
* When `content_id` is omitted, the filename is used as the Content-ID, so `` also works.
* Inline images are delivered inside a `multipart/related` MIME part alongside the HTML body, so email clients embed them rather than showing them as downloadable attachments.
* Because the image travels inside the message body, it is never fetched or logged by link proxies or image-caching services — suitable for per-recipient secrets like QR codes.
* On Mailgun, the `content_id` also becomes the delivered part's filename, so prefer a `content_id` with a file extension (e.g. `checkInQr.png`) when sending through Mailgun.
### Common Use Cases
* **Order Confirmations**: Attach invoices and receipts
* **Booking Confirmations**: Attach tickets and itineraries
* **Legal Documents**: Attach contracts and agreements
* **Reports**: Attach analytics reports and summaries
* **Certificates**: Attach completion certificates and badges
* **Event Tickets**: Embed per-recipient QR codes as inline images
## SMTP Bridge
Notifuse includes a built-in SMTP bridge server that allows you to send transactional emails using standard SMTP clients instead of HTTP API calls. This is particularly useful when:
* **Legacy Systems**: Integrating with existing applications that only support SMTP
* **Email Libraries**: Using standard email libraries in any programming language
* **Framework Integration**: Working with frameworks that have built-in SMTP support
* **Gradual Migration**: Moving from existing email providers without changing application code
### How It Works
The SMTP bridge server accepts emails via SMTP on port 587 (STARTTLS) or 465 (implicit TLS). The email body must contain a JSON payload matching the Transactional API format. Authentication is done using your workspace API credentials.
### Sending Emails via SMTP
#### Authentication
* **Username**: Your workspace API email (the email associated with your API key)
* **Password**: Your workspace API key
* **Security**: STARTTLS (port 587) or implicit TLS / SMTPS (port 465), depending on your `SMTP_BRIDGE_TLS` setting
* **Server**: Your configured SMTP bridge host (e.g., `smtp.yourdomain.com`)
#### Email Format
The email body must be a JSON payload matching the Transactional API format:
```json theme={null}
{
"workspace_id": "your_workspace",
"notification": {
"id": "password_reset",
"external_id": "unique-id-123",
"channels": ["email"],
"contact": {
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe"
},
"data": {
"reset_token": "abc123",
"expiry_time": "1 hour"
}
}
}
```
## Permissions
The **Transactional** write permission on its own is enough to send, over both the API and the SMTP bridge. That makes a send-only API key practical: create a key, grant it Transactional write, leave everything else off, and it can send email without being able to read or edit a single contact. See [Admins & API Keys](/features/admins-api-keys).
Two limits apply to a key scoped that narrowly, and they apply identically to the SMTP bridge:
* **Without Contacts write**, a send still creates the recipient's contact when it does not exist yet, but only their email address is stored. Every other field of the `contact` object is dropped, and an existing contact is never modified.
* **Without Contacts read**, a send that sets `cc` or `bcc` is refused. On the SMTP bridge those come from the message's `Cc:` and `Bcc:` headers, so a send-only credential that sets them is rejected as the message is handed over.
Add Contacts read and write to the key if it needs to enrich contact profiles as it sends, or to copy extra recipients. A key holding both behaves exactly as it did before.
Transactional write covers every path that sends real mail through your provider credentials, not just `/api/transactional.send`: it also covers `/api/transactional.testTemplate` and `/api/email.testProvider`, which despite its name delivers a real message.
Transactional **read** covers only the notification definitions — listing them and opening one. It does not let a key send.
## Template Integration
### Data Structure in Templates
Your templates have access to both API data and contact profile:
```liquid theme={null}
Hello! Your order #{{ order_id }} has been confirmed.
Dear {{ contact.first_name }} {{ contact.last_name }},
We've sent your order #{{ order_id }} to {{ contact.email }}.
```
### Complete Example
**API Request**:
```json theme={null}
{
"workspace_id": "your_workspace",
"notification": {
"id": "order_confirmation",
"external_id": "order-12345-confirmation",
"contact": {
"email": "sarah@example.com"
},
"data": {
"order_id": "ORD-12345",
"total": "$99.99",
"items": ["Product A", "Product B"]
},
"metadata": {
"campaign_id": "order-confirmations",
"source": "checkout"
}
}
}
```
**Existing Contact Profile**:
```json theme={null}
{
"first_name": "Sarah",
"last_name": "Johnson",
"email": "sarah@example.com",
"phone": "+1234567890"
}
```
**Available in Template**:
```liquid theme={null}
Hi {{ contact.first_name }}!
Your order #{{ order_id }} totaling {{ total }} has been confirmed.
Items:
{% for item in items %}
- {{ item }}
{% endfor %}
We'll send updates to {{ contact.email }}.
```
## API Reference
## Deduplication Examples
### Order Confirmations
```json theme={null}
{
"external_id": "order-12345-confirmation"
}
```
### Password Resets
```json theme={null}
{
"external_id": "pwd-reset-user123-20240101"
}
```
### Welcome Emails
```json theme={null}
{
"external_id": "welcome-user123"
}
```
Using consistent `external_id` patterns ensures that duplicate notifications are automatically prevented, even if your application sends multiple requests.
For complete API documentation and additional parameters, see the [API Reference](/api-reference/send-a-transactional-notification).
# Webhooks
Source: https://docs.notifuse.com/features/webhooks
Receive real-time HTTP callbacks when events occur in your workspace.
## Overview
Notifuse handles two types of webhooks:
1. **Incoming webhooks** - Callbacks from email providers (Amazon SES, Mailgun, Postmark, etc.) that notify Notifuse about email events like bounces, complaints, and deliveries. These are configured automatically when you set up an email integration.
2. **Outgoing webhooks** - HTTP callbacks that Notifuse sends to *your* server when events occur. This is what this feature page covers.
Outgoing webhooks allow you to receive HTTP POST requests to your server when specific events occur in Notifuse. This enables you to:
* Sync contact data to your CRM or other systems
* Trigger workflows in external automation tools
* Build custom integrations and dashboards
* React to email engagement events in real-time
## Creating a Webhook Subscription
Create a webhook subscription via the API or the Notifuse console:
```bash theme={null}
curl -X POST "https://your-instance.com/api/webhookSubscriptions.create" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "your_workspace",
"name": "Production Webhook",
"url": "https://api.example.com/webhooks/notifuse",
"event_types": ["contact.created", "contact.updated", "email.sent"]
}'
```
## Available Event Types
### Contact Events
| Event | Description |
| - | - |
| `contact.created` | A new contact was added to the workspace |
| `contact.updated` | Contact data was modified |
| `contact.deleted` | A contact was permanently deleted |
### List Events
| Event | Description |
| - | - |
| `list.subscribed` | Contact subscribed to a list |
| `list.unsubscribed` | Contact unsubscribed from a list |
| `list.confirmed` | Contact confirmed double opt-in subscription |
| `list.resubscribed` | Previously unsubscribed contact resubscribed |
| `list.bounced` | Email to contact bounced |
| `list.complained` | Contact marked email as spam |
| `list.pending` | Contact subscription is awaiting confirmation |
| `list.removed` | Contact was removed from list |
### Segment Events
| Event | Description |
| - | - |
| `segment.joined` | Contact entered a segment (matched segment criteria) |
| `segment.left` | Contact left a segment (no longer matches criteria) |
### Email Events
| Event | Description |
| - | - |
| `email.sent` | Email was sent to contact |
| `email.delivered` | Email delivery was confirmed by the recipient's server |
| `email.opened` | Contact opened the email |
| `email.clicked` | Contact clicked a link in the email |
| `email.bounced` | Email delivery failed |
| `email.complained` | Contact marked email as spam |
| `email.unsubscribed` | Contact unsubscribed via email link |
### Custom Events
| Event | Description |
| - | - |
| `custom_event.created` | A new custom event was recorded |
| `custom_event.updated` | A custom event was updated |
| `custom_event.deleted` | A custom event was soft-deleted |
#### Custom Event Filters
For custom events, you can optionally filter by `goal_types` and `event_names`:
```json theme={null}
{
"event_types": ["custom_event.created"],
"custom_event_filters": {
"goal_types": ["purchase", "subscription"],
"event_names": ["orders/fulfilled", "payment.succeeded"]
}
}
```
## Filtering by List and Segment
By default a subscription to `list.*` events receives an event for **every** list in the workspace, and a subscription to `segment.*` events one for every segment. Narrow that with `list_ids` and `segment_ids`:
```json theme={null}
{
"event_types": ["list.subscribed", "list.unsubscribed", "segment.joined"],
"list_ids": ["newsletter", "product_updates"],
"segment_ids": ["vip_customers"]
}
```
* `list_ids` applies to the `list.*` events only, `segment_ids` to the `segment.*` events only. Neither affects contact, email or custom event types.
* Omitting a filter, or sending an empty array, means "every list" or "every segment" — which is how every subscription created before these filters existed keeps behaving.
* Both filters can be changed later with `webhookSubscriptions.update`.
Filter whenever you can. A workspace with large lists generates one delivery per contact per list event, so a subscription that only cares about one list but subscribes to all of them does real work on your instance for events it will discard.
## Subscription Fields
Alongside its name, URL and event types, a subscription reports:
| Field | Description |
| - | - |
| `enabled` | Whether the subscription is currently delivering |
| `source` | What created it. Absent for a subscription you created yourself; `zapier` for one created by the [Zapier integration](/integrations/zapier) |
| `consecutive_failures` | Delivery attempts that have failed back to back. Reset to zero by the first success |
| `failing_since` | When the current run of failures started. Cleared by the first success |
| `disabled_reason` | Why the subscription was switched off automatically. Absent for one you switched off yourself |
| `last_delivery_at` | When a delivery last succeeded |
A subscription carrying a `source` was created by another tool and is owned by it. Deleting a Zapier-created subscription from Notifuse silently breaks the Zap behind it, and the Zap will not recreate it on its own. Turn the Zap off in Zapier instead.
### When Notifuse Switches a Subscription Off
An endpoint that stops answering is retired automatically, but only once it has been failing persistently. Both conditions have to be met: 20 consecutive failures, and a run of them that has been going for more than two hours. A short outage — a deploy, a restart, a receiver that is briefly unreachable during an import — never switches your subscription off, however many deliveries it refuses in the meantime. Those deliveries are retried on the usual backoff.
An endpoint answering `410 Gone` is the exception and is retired at once, because that status means the receiver is telling you the subscription no longer exists.
Switching a subscription back on clears its failure history, so it starts from a clean slate rather than being retired again on the next hiccup.
### Where Notifuse Can Deliver
Your endpoint has to be reachable from the public internet. Notifuse refuses to connect to private, loopback, link-local and reserved addresses — including the cloud instance metadata endpoint — and checks this again on every redirect, so a public URL that redirects to an internal one is refused as well. A subscription whose URL names such an address is rejected when you save it.
Self-hosted only: if you run Notifuse yourself and deliberately deliver webhooks to a service on your own private network — a container on the same Docker network, an internal automation tool — set `WEBHOOK_DELIVERY_ALLOW_PRIVATE_HOSTS=true`. Leave it unset on any deployment where someone you would not trust with your internal network can create subscriptions.
A delivery refused this way is retried like any other failure, but it never counts toward switching the subscription off, so correcting the URL or the setting is enough to get it delivering again.
## Payload Structure
All webhook payloads follow this structure:
```json theme={null}
{
"id": "delivery_abc123",
"type": "contact.created",
"timestamp": "2024-01-15T10:30:00Z",
"workspace_id": "ws_1234567890",
"data": {
// Event-specific data
}
}
```
| Field | Description |
| - | - |
| `id` | Unique identifier for the webhook delivery |
| `type` | The event type that triggered the webhook |
| `timestamp` | ISO 8601 timestamp when the webhook was sent |
| `workspace_id` | The workspace where the event occurred |
| `data` | Event-specific payload data |
### Example: Contact Created
```json theme={null}
{
"id": "delivery_abc123",
"type": "contact.created",
"timestamp": "2024-01-15T10:30:00Z",
"workspace_id": "ws_1234567890",
"data": {
"email": "user@example.com",
"id": "contact_12345",
"external_id": "ext_67890",
"first_name": "John",
"last_name": "Doe",
"tags": ["newsletter", "customer"],
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
}
```
### Example: Email Sent
```json theme={null}
{
"id": "delivery_def456",
"type": "email.sent",
"timestamp": "2024-01-15T10:30:00Z",
"workspace_id": "ws_1234567890",
"data": {
"message_id": "msg_xyz789",
"email": "user@example.com",
"subject": "Welcome to our newsletter",
"broadcast_id": "bc_abc123",
"template_id": "welcome_email",
"created_at": "2024-01-15T10:30:00Z"
}
}
```
### Example: Email Opened
```json theme={null}
{
"id": "delivery_ghi789",
"type": "email.opened",
"timestamp": "2024-01-15T11:45:00Z",
"workspace_id": "ws_1234567890",
"data": {
"message_id": "msg_xyz789",
"email": "user@example.com",
"subject": "Welcome to our newsletter",
"broadcast_id": "bc_abc123",
"template_id": "welcome_email",
"created_at": "2024-01-15T11:45:00Z"
}
}
```
### Example: Segment Joined
```json theme={null}
{
"id": "delivery_jkl012",
"type": "segment.joined",
"timestamp": "2024-01-15T10:30:00Z",
"workspace_id": "ws_1234567890",
"data": {
"email": "user@example.com",
"contact_id": "contact_12345",
"segment_id": "seg_vip_customers",
"segment_name": "VIP Customers",
"created_at": "2024-01-15T10:30:00Z"
}
}
```
## Verifying Webhook Signatures
Notifuse signs all webhook payloads using the [Standard Webhooks](https://www.standardwebhooks.com/) specification with HMAC-SHA256.
### Headers
Each webhook request includes these headers:
| Header | Description |
| - | - |
| `webhook-id` | Unique identifier for the webhook delivery |
| `webhook-timestamp` | Unix timestamp when the webhook was sent |
| `webhook-signature` | HMAC-SHA256 signature of the payload |
### Verifying the Signature
1. Extract the timestamp and signature from the headers
2. Construct the signed payload: `{webhook-id}.{webhook-timestamp}.{body}`
3. Compute HMAC-SHA256 using your webhook secret
4. Compare with the provided signature
### Code Examples
```javascript Node.js theme={null}
const crypto = require('crypto');
function verifyWebhook(payload, headers, secret) {
const msgId = headers['webhook-id'];
const msgTimestamp = headers['webhook-timestamp'];
const msgSignature = headers['webhook-signature'];
// Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(msgTimestamp)) > 300) {
throw new Error('Timestamp too old');
}
// Construct signed payload
const signedPayload = `${msgId}.${msgTimestamp}.${payload}`;
// Compute signature
const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64');
const expectedSignature = crypto
.createHmac('sha256', secretBytes)
.update(signedPayload)
.digest('base64');
// Extract v1 signature
const signatures = msgSignature.split(' ');
for (const sig of signatures) {
const [version, signature] = sig.split(',');
if (version === 'v1' && signature === expectedSignature) {
return true;
}
}
throw new Error('Invalid signature');
}
```
```python Python theme={null}
import hmac
import hashlib
import base64
import time
def verify_webhook(payload: str, headers: dict, secret: str) -> bool:
msg_id = headers.get('webhook-id')
msg_timestamp = headers.get('webhook-timestamp')
msg_signature = headers.get('webhook-signature')
# Check timestamp is within 5 minutes
now = int(time.time())
if abs(now - int(msg_timestamp)) > 300:
raise ValueError('Timestamp too old')
# Construct signed payload
signed_payload = f"{msg_id}.{msg_timestamp}.{payload}"
# Compute signature
secret_bytes = base64.b64decode(secret.replace('whsec_', ''))
expected_signature = base64.b64encode(
hmac.new(secret_bytes, signed_payload.encode(), hashlib.sha256).digest()
).decode()
# Extract v1 signature
for sig in msg_signature.split(' '):
version, signature = sig.split(',')
if version == 'v1' and signature == expected_signature:
return True
raise ValueError('Invalid signature')
```
```go Go theme={null}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"math"
"strconv"
"strings"
"time"
)
func VerifyWebhook(payload string, headers map[string]string, secret string) error {
msgID := headers["webhook-id"]
msgTimestamp := headers["webhook-timestamp"]
msgSignature := headers["webhook-signature"]
// Check timestamp is within 5 minutes
ts, _ := strconv.ParseInt(msgTimestamp, 10, 64)
now := time.Now().Unix()
if math.Abs(float64(now-ts)) > 300 {
return errors.New("timestamp too old")
}
// Construct signed payload
signedPayload := fmt.Sprintf("%s.%s.%s", msgID, msgTimestamp, payload)
// Compute signature
secretBytes, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
h := hmac.New(sha256.New, secretBytes)
h.Write([]byte(signedPayload))
expectedSig := base64.StdEncoding.EncodeToString(h.Sum(nil))
// Extract v1 signature
for _, sig := range strings.Split(msgSignature, " ") {
parts := strings.SplitN(sig, ",", 2)
if len(parts) == 2 && parts[0] == "v1" && parts[1] == expectedSig {
return nil
}
}
return errors.New("invalid signature")
}
```
```php PHP theme={null}
300) {
throw new Exception('Timestamp too old');
}
// Construct signed payload
$signedPayload = "{$msgId}.{$msgTimestamp}.{$payload}";
// Compute signature
$secretBytes = base64_decode(str_replace('whsec_', '', $secret));
$expectedSignature = base64_encode(
hash_hmac('sha256', $signedPayload, $secretBytes, true)
);
// Extract v1 signature
foreach (explode(' ', $msgSignature) as $sig) {
[$version, $signature] = explode(',', $sig, 2);
if ($version === 'v1' && $signature === $expectedSignature) {
return true;
}
}
throw new Exception('Invalid signature');
}
```
## Retry Behavior
Notifuse automatically retries failed webhook deliveries with an exponential backoff strategy.
### Retry Schedule
Webhooks are retried up to **10 times** over approximately **34 hours**:
| Attempt | Delay After Previous |
| - | - |
| 1 | Immediate |
| 2 | 30 seconds |
| 3 | 1 minute |
| 4 | 2 minutes |
| 5 | 5 minutes |
| 6 | 15 minutes |
| 7 | 30 minutes |
| 8 | 1 hour |
| 9 | 2 hours |
| 10 | 6 hours |
| Final | 24 hours |
### Success Criteria
A webhook delivery is considered successful if your endpoint returns HTTP status code `2xx` (200-299).
### Failure Conditions
A delivery attempt fails if:
* Your endpoint returns a non-2xx status code
* The request times out (30 second timeout)
* The endpoint is unreachable (DNS error, connection refused, etc.)
## Automatic Disabling
An endpoint that has moved, been decommissioned or lost its DNS record never comes back on its own, and Notifuse will not keep queueing deliveries for it forever.
**After a long run of back-to-back failures, a subscription is switched off automatically** and the reason is recorded on it, so a webhook you find disabled can be told apart from one you disabled yourself. The counter is per subscription and a single successful delivery resets it to zero, so an endpoint that is merely having a bad afternoon is not switched off — only one that fails persistently.
Two responses are treated specially:
* **`429 Too Many Requests` never counts as a failure.** Rate limiting is your server asking for less, not an endpoint dying, and a workspace busy enough to be throttled should not have its integration switched off for being busy. The delivery is retried on the normal schedule.
* **`410 Gone` is terminal on the first response.** It is the standard way for a receiver to say "this subscription is dead, stop sending", so the subscription is retired immediately rather than after the usual run of failures. A subscription you created is switched off with that reason recorded; one created by another tool, such as Zapier, is removed outright, because the tool recreates it when it needs it. `404` is *not* treated this way — an endpoint that is briefly missing is retried like any other failure.
To bring a disabled subscription back, fix the endpoint and re-enable it.
**A disabled subscription drops events rather than queueing them.** Deliveries that come up while a subscription is off — whether you switched it off or Notifuse did — are marked failed in the delivery log and are not replayed when you turn it back on. If you need the events that occurred during an outage, read them from the API rather than expecting a backlog.
## Deleting a Subscription
Deleting a subscription also discards the deliveries still queued for it. There is nothing left behind to retry, and no need to drain a queue before deleting.
Disable a subscription instead of deleting it when you expect to bring the endpoint back: disabling is reversible, and the subscription — with its signing secret and its delivery history — survives. A deleted subscription cannot be restored, and recreating it issues a new signing secret.
## Best Practices
### Return 2xx Quickly
Return a successful response as quickly as possible. Process the webhook payload asynchronously if needed:
```javascript theme={null}
// Good: Acknowledge quickly, process later
app.post('/webhooks/notifuse', async (req, res) => {
// Quickly validate signature
verifyWebhook(req.body, req.headers, SECRET);
// Queue for async processing
await queue.add('process-webhook', req.body);
// Respond immediately
res.status(200).json({ received: true });
});
```
### Handle Duplicate Deliveries
Webhooks may be delivered more than once in rare cases. Use the `webhook-id` header to implement idempotency:
```javascript theme={null}
app.post('/webhooks/notifuse', async (req, res) => {
const webhookId = req.headers['webhook-id'];
// Check if we've already processed this webhook
if (await hasProcessed(webhookId)) {
return res.status(200).json({ received: true, duplicate: true });
}
// Mark as processed before handling
await markProcessed(webhookId);
// Process the webhook
await processWebhook(req.body);
res.status(200).json({ received: true });
});
```
### Verify Signatures
Always verify webhook signatures in production to ensure requests are from Notifuse.
### Monitor Delivery Failures
Use the Notifuse console or API to monitor webhook delivery status:
```bash theme={null}
curl "https://your-instance.com/api/webhookSubscriptions.deliveries?workspace_id=ws_123&subscription_id=whsub_456" \
-H "Authorization: Bearer YOUR_API_KEY"
```
## Testing Webhooks
### Send a Test Webhook
Use the test endpoint to verify your webhook configuration:
```bash theme={null}
curl -X POST "https://your-instance.com/api/webhookSubscriptions.test" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "your_workspace",
"id": "whsub_a1b2c3d4e5f6",
"event_type": "contact.created"
}'
```
The `event_type` parameter is optional. If provided, the test webhook will include a realistic sample payload for that event type. If omitted, a generic test payload is sent.
### Local Development
For local development, use tools like [ngrok](https://ngrok.com/) or [localtunnel](https://localtunnel.github.io/www/) to expose your local server:
```bash theme={null}
# Start ngrok
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/notifuse
```
## Permissions
Outgoing subscriptions follow the **Webhook Subscriptions** permission: **read** lists subscriptions, opens one, and shows its delivery history — including each event payload and the endpoint's response status and body. **Write** creates, updates, deletes, enables and disables subscriptions, and fires a test delivery. Firing a test is a write, not a read, because it makes your server call out.
Incoming provider callbacks follow the **Webhook Events** permission: **read** shows the raw callbacks your email providers have sent back to Notifuse, including recipient addresses and provider diagnostics. **Webhook Events write gates nothing today** — the rows arrive on the provider callback routes, which carry no session and consult no permission, so granting it confers nothing. The console shows it granted and locked.
The catalogue of available event types is not gated at all: any authenticated member or key can list it.
Two actions stay reserved for the workspace owner, whatever permissions a member or API key holds:
* **Regenerating a subscription's signing secret.** The secret is also hidden from everyone but the owner, so Webhook Subscriptions read never hands it out — a member can list subscriptions and inspect deliveries without ever seeing the key those payloads are signed with.
* **Registering webhooks with an email provider**, because it uses the provider credentials stored in your integration.
Webhook Subscriptions **write** reaches further than its name suggests. A subscription chooses the
events it receives, so whoever holds this permission can subscribe to contact and email events and
have that data delivered to a URL of their choosing. Grant it as you would grant read access to
the contacts and the message history behind those events.
## Notes
* Webhooks require HTTPS endpoints in production
* The signing secret is generated automatically when you create a subscription
* Use `webhookSubscriptions.regenerateSecret` to rotate the signing secret
* See [API Reference](/api-reference) for complete endpoint documentation
# Workspaces
Source: https://docs.notifuse.com/features/workspaces
Workspaces are the foundational organizational unit in Notifuse. They provide a complete, isolated environment for managing your email marketing operations, making it easy to separate different projects, clients, or business units.
**Self-hosted installations get three workspaces without a licence key.** Creating a fourth needs a [licence](/self-hosting/licence); workspaces you already have are never removed, disabled or hidden, whatever the licence state. Notifuse Cloud plans include their own workspace count. The isolation itself — one PostgreSQL database per workspace — is never licensed, only the count beyond three.
## What is a Workspace?
A workspace in Notifuse is a self-contained environment that includes:
* **Contacts and Lists**: Your subscriber database and segmentation
* **Email Templates**: Custom designs and layouts
* **Campaigns**: Broadcast and automated email sequences
* **Transactional Notifications**: Event-triggered emails
* **Message & webhooks History**: All messages sent to the contact and webhooks received
* **Settings**: Email providers and configurations
* **Custom Endpoint URL**: Workspace-specific domain for click tracking, unsubscribe links, and notification center
## Custom Endpoint URL
Each workspace can be configured with a custom endpoint URL to provide a branded experience for your recipients. This custom domain is used for:
* **Click Tracking**: All tracked links in your emails will use your custom domain
* **Unsubscribe Links**: Recipients will see your branded domain when unsubscribing
* **Notification Center**: Your notification center will be accessible via your custom domain
This feature allows you to maintain brand consistency and build trust with your recipients by using your own domain instead of the default Notifuse domain.
## Who Can Change a Workspace
Renaming a workspace, changing its general settings, deleting it, and creating, updating or deleting an integration are **owner-only**. No permission grants them, and granting the Workspace permission does not confer them.
The **Workspace** permission is narrower than its name suggests: **read** opens the workspace record and the team roster, and **write** adds exactly one setting — renaming the custom contact field labels — plus running, resetting and deleting integration sync tasks. Blog settings answer to Blog write, and web analytics settings to Web Analytics write. See [Admins & API Keys](/features/admins-api-keys).
## Multi-Tenant Architecture
* **Complete Isolation**: Each workspace operates independently
* **Separate Databases**: Contacts, campaigns, and data are isolated
* **Individual Settings**: Each workspace has its own email providers and configurations
* **Custom Branding**: Each workspace can use its own domain for all recipient-facing URLs
# Welcome to Notifuse
Source: https://docs.notifuse.com/index
Notifuse is a modern emailing platform for businesses that want complete control over their email infrastructure. Whether you're sending marketing campaigns, transactional emails, or building notification systems, Notifuse provides the tools and flexibility you need.
## Features
Get familiar with Notifuse's fundamental building blocks:
Isolated environments for organizing your email operations by project, client, or business unit.
Manage your subscriber database with powerful segmentation and list management tools.
Create reusable email designs with our visual builder and template system.
Send targeted marketing emails to specific segments of your audience.
Send automated emails triggered by user actions or system events.
Track user behavior and conversions to build RFM segments.
Cookieless website analytics with goals, attribution and annotations.
Full-featured blogging system with Liquid theming.
Write posts, design emails and query your analytics, on your own AI provider key.
Embedded notification system for your applications.
An append-only trail of who did what, with export and a retention you control.
## Email Provider Integrations
Notifuse supports multiple email service providers, giving you flexibility and redundancy:
Cost-effective email service from AWS
Powerful email API for developers
Fast and reliable transactional email
Reliable email delivery at scale
Enterprise email delivery platform
All-in-one email service solution
Send emails via your own SMTP server
## Platform Integrations
Seamlessly integrate Notifuse with your existing tools and workflows:
Claude models for the AI assistant
GPT models, or any OpenAI-compatible API
Gemini models for the AI assistant
Web access for AI research and content
Auth emails and user sync integration
## Getting Started
Get started in minutes with our managed cloud platform — from \$16/month.
Explore our comprehensive API documentation and start integrating.
Try Notifuse instantly with our interactive demo.
# Amazon SES
Source: https://docs.notifuse.com/integrations/amazon-ses
Configure Amazon SES as your email provider in Notifuse.
Amazon SES integration allows you to use AWS Simple Email Service for sending emails through Notifuse.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Amazon SES**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **AWS Region** | AWS region dropdown select | `us-east-1` |
| **AWS Access Key** | AWS access key with SES permissions | `AKIAIOSFODNN7EXAMPLE` |
| **AWS Secret Key** | AWS secret access key | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
| **SES tenant isolation** | Optional. Give this integration its own reputation profile and suppression list — see [below](#reputation-isolation-ses-tenants) | Off |
| **Configuration set** | Optional, under **Advanced**. Send through a configuration set you manage | `my-config-set` |
| **SES tenant** | Optional, under **Advanced**. Send through a tenant you manage | `my-tenant` |
### Senders List
Add sender addresses that have been verified in your AWS SES settings:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Support Team` |
| **Email** | Sender email address | `support@yourdomain.com` |
**Important**: Senders must be verified in AWS SES console before they can be used.
### Usage Types
Configure how this Amazon SES integration will be used:
| Option | Description | Examples |
| - | - | - |
| **Use for Transactional** | System-generated emails triggered by user actions | Password resets, order confirmations, account notifications |
| **Use for Marketing** | Promotional and newsletter emails sent to subscribers | Product announcements, newsletters, promotional campaigns |
You can enable both options to use the same SES configuration for all email types.
### Webhooks
Notifuse automatically subscribes to the following SES webhooks:
| Webhook Type | Description | Purpose |
| - | - | - |
| **Delivery** | Email successfully delivered | Track successful deliveries |
| **Bounce** | Email bounced | Monitor bounces |
| **Complaints** | Recipient filed abuse complaint | Handle abuse reports |
These webhooks provide real-time feedback on email performance and delivery status.
## Reputation isolation (SES tenants)
By default every workspace in an AWS account shares one sending reputation and **one suppression list**. A badly targeted campaign from one team can push the account's bounce or complaint rate over AWS's thresholds and pause sending for everyone, and a hard bounce recorded for one workspace suppresses that recipient for all the others.
Turning on **SES tenant isolation** gives an integration its own [SES tenant](https://docs.aws.amazon.com/ses/latest/dg/tenants.html): its own reputation profile, its own bounce and complaint metrics, and its own suppression list. AWS can then pause a single tenant instead of the whole account.
**Self-hosted: provisioning a new SES tenant needs a [licence key](/self-hosting/licence).** A tenant already provisioned keeps sending through its own reputation indefinitely without one — the send path contains no licence check. Every Notifuse Cloud plan includes it.
### Enabling it
Open the integration (**Settings** → **Integrations**), switch **SES tenant isolation** on, and save. Notifuse confirms first — a tenant is a billable AWS resource — then provisions everything in your account:
| Step | What Notifuse does |
| - | - |
| **Create the tenant** | Named `notifuse-`, with `SuppressionScope: TENANT` for bounces and complaints |
| **Associate the config set** | Attaches the configuration set this integration sends through |
| **Associate identities** | Attaches every verified identity that covers a sender — the exact address, its domain, or a parent domain |
Saving again re-runs the same steps. That is deliberate: it is how a sender you add later gets associated, and how a partial run is retried.
**Tenant-level suppression is not the AWS default.** A tenant created by hand in the SES console inherits the account-level suppression list unless you set the suppression scope yourself — giving you reputation isolation and silently *not* suppression isolation. Notifuse sets it at creation, and corrects the scope if it finds a tenant of that name already exists.
Isolation is scoped to the **integration**, not the workspace. A workspace running separate marketing and transactional SES integrations gets two tenants, each with its own suppression list, so a bounce recorded by one is not suppressed by SES on the other. Notifuse's own per-workspace suppression still applies to both, so the practical exposure is a single extra send to an address SES already knows is bad.
### What you'll see
* **A sender with no verified identity** is reported by name after saving. Those sends would fail with or without tenants, so it is worth fixing.
* **A missing IAM permission** is reported with the exact action and an `aws` CLI command to run. Notifuse does not switch the integration over to the tenant until its configuration set is associated: sending through a tenant whose configuration set is not attached makes SES reject **every** message, so a partial setup leaves sending exactly as it was.
* The integration card shows the configuration set in use and whether reputation is `isolated` or `shared with the rest of this AWS account`.
### Turning it off
Switching **SES tenant isolation** off takes effect on the next send: messages stop being scoped to the tenant and go out under the account's shared reputation again. The tenant itself is left in AWS, so its suppression list and reputation history survive — switching back on resumes the same tenant rather than starting from zero.
A tenant that isolation was switched off for **keeps billing** until the integration is deleted. Turn it off to change how you send; delete the integration to stop the charge.
**Deleting the integration deletes the tenant**, along with its suppression list and reputation history — those cannot be recreated. Removing only the webhooks leaves both in place, because sending still depends on them.
### Advanced: bring your own tenant or configuration set
Under **Advanced**, both fields are optional and suggest the names that actually exist in your account (they fall back to plain text if your credentials cannot list them):
| Field | Description |
| - | - |
| **Configuration set** | Send through a configuration set you manage instead of the `notifuse-` one. Notifuse attaches its event destination to it but never creates or deletes it. |
| **SES tenant** | Send through a tenant you manage yourself. Mutually exclusive with the switch above — Notifuse will not create, modify or delete it. |
Choosing your own tenant means you are responsible for associating the sending identity **and** the configuration set with it; otherwise SES rejects every send:
```bash theme={null}
aws sesv2 create-tenant-resource-association \
--tenant-name my-tenant \
--resource-arn arn:aws:ses:::configuration-set/
```
Tenants exist only in the SES API v2, which is what Notifuse now sends through. They are also region-scoped, so a tenant lives in the same region as its integration.
## Inbound replies (stop-on-reply)
To use **[Exit on reply](/features/automations#exit-on-reply)** in automations, SES must forward inbound replies back to Notifuse. SES delivers received mail differently from sending it, so this is a one-time setup with two parts:
1. **Point your domain's MX records at SES.** Add an MX record on your sending domain pointing at the SES inbound endpoint for your region—`inbound-smtp..amazonaws.com` (for example `inbound-smtp.us-east-1.amazonaws.com`), at priority `10`. Without it, replies never reach SES. SES has no API for DNS, so this step is manual.
2. **Register the inbound route.** Click **Register Webhooks** on the SES integration (**Settings → Integrations**). Alongside the delivery, bounce, and complaint event webhooks, Notifuse provisions an SNS topic (HTTPS-subscribed to Notifuse, signed with SignatureVersion 2) and an SES **receipt rule** with an SNS action. The rule is added **into your account's active receipt rule set** when one exists—it only forwards a copy and has no *stop* action, so it won't disrupt an existing setup (such as Amazon WorkMail). A dedicated `notifuse-inbound` rule set is created and activated **only** if no rule set is currently active.
SES email receiving is only available in a subset of AWS regions (for example `us-east-1`, `us-west-2`, `eu-west-1`). If your integration's region doesn't support receiving, inbound registration reports as unavailable. See the AWS [Email Receiving endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) list.
The inbound SNS topic, receipt rule, and subscription require IAM permissions beyond sending: `sns:CreateTopic`, `sns:SetTopicAttributes`, `sns:Subscribe`, `ses:DescribeActiveReceiptRuleSet`, `ses:CreateReceiptRuleSet`, `ses:CreateReceiptRule`, `ses:SetActiveReceiptRuleSet`, `ses:DeleteReceiptRule`, and `ses:ListIdentities`. The receipt rule is scoped to your account's verified identities (both domain and email-address identities); if none are verified, inbound registration is refused rather than forwarding all account mail.
Replies are matched to the original send by their `Message-ID`. SES overwrites the `Message-ID` header at send time, so Notifuse captures the value SES returns and matches it host-independently—no separate inbound signing key is required. Each inbound SNS notification is **RSA signature-verified** and **bound to the provisioned topic** (a message from any other SNS topic, even with a valid AWS signature, is rejected) before it is trusted, and the endpoint is rate-limited.
**Manual setups must record the SNS topic ARN.** Because inbound messages are authenticated against the provisioned topic, an integration with no bound topic ARN rejects all inbound mail. The **Register Webhooks** action records it automatically. If you wire SES inbound by hand (your own SNS topic + receipt rule), set the topic ARN on the SES integration's `inbound_topic_arn` setting, otherwise replies won't be accepted.
Once both parts are in place, a contact's reply to an automation email will exit their journey whenever that automation has **Exit on reply** enabled. Bounces and out-of-office auto-replies are filtered out and never count as a reply.
## Prerequisites
Before configuring Amazon SES:
1. **AWS Account**: Active AWS account with SES enabled
2. **Verified Senders**: Email addresses or domains verified in AWS SES console
3. **IAM Permissions**: AWS user with SES sending permissions — see below
4. **Production Access**: Move out of sandbox mode for unrestricted sending
### IAM permissions
Only the first group is required. Everything else unlocks an optional feature and degrades cleanly when absent — a missing permission is reported with the action to add, never a silent failure.
| Purpose | Actions |
| - | - |
| **Sending** (required) | `ses:SendEmail` |
| **Event webhooks** | `ses:CreateConfigurationSet`, `ses:DescribeConfigurationSet`, `ses:ListConfigurationSets`, `ses:DeleteConfigurationSet`, `ses:CreateConfigurationSetEventDestination`, `ses:UpdateConfigurationSetEventDestination`, `ses:DeleteConfigurationSetEventDestination`, `sns:CreateTopic`, `sns:Subscribe`, `sns:GetTopicAttributes`, `sns:DeleteTopic` |
| **Reputation isolation** | `ses:CreateTenant`, `ses:GetTenant`, `ses:PutTenantSuppressionAttributes`, `ses:CreateTenantResourceAssociation`, `ses:ListEmailIdentities` |
| **Name pickers and checks** | `ses:ListTenants`, `ses:ListTenantResources` |
| **Clean removal** | `ses:DeleteTenant`, `ses:DeleteTenantResourceAssociation` |
| **Inbound replies** | See the [stop-on-reply note](#inbound-replies-stop-on-reply) above |
Without the **clean removal** permissions, deleting an integration leaves its tenant behind in your AWS account — where it keeps billing until you remove it by hand.
## Testing
Click **Test Integration** to verify your configuration.
# Anthropic
Source: https://docs.notifuse.com/integrations/anthropic
Configure Anthropic Claude AI to power blog content creation and email generation in Notifuse.
The Anthropic integration enables AI-powered features across Notifuse using Claude, Anthropic's advanced AI assistant. This integration powers the [AI assistant](/features/ai-assistant) for blog content creation, email design and web analytics.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Anthropic**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Key** | Your Anthropic API key | `sk-ant-api03-...` |
| **Model** | Select the Claude model to use | `claude-sonnet-4-6` |
### Available Models
| Model | Model ID | Description | \$ Pricing (per MTok) |
| - | - | - | - |
| **Claude Opus 4.6** | `claude-opus-4-6` | Most capable model for complex reasoning and creative tasks | $5 input / $25 output |
| **Claude Sonnet 4.6** | `claude-sonnet-4-6` | Balanced performance and cost for most use cases | $3 input / $15 output |
| **Claude Haiku 4.5** | `claude-haiku-4-5-20251001` | Fast and cost-effective for simpler tasks | $1 input / $5 output |
The assistant shows the running cost of a conversation from these prices. See [Anthropic's pricing page](https://claude.com/pricing) for the authoritative rates.
## Features Enabled
Once configured, the Anthropic integration enables the following AI-powered features:
### Blog AI Agent
The AI agent can assist with blog content creation:
| Feature | Description |
| - | - |
| **Write blog posts** | Generate complete blog articles based on topics or brief descriptions |
| **SEO optimization** | Automatically generate SEO-friendly meta titles, descriptions, and keywords |
| **Content editing** | Improve, expand, or summarize existing content |
### Email Editor AI
Generate emails automatically using AI:
| Feature | Description |
| - | - |
| **Email generation** | Create email content from prompts or descriptions |
| **Subject lines** | Generate compelling subject lines for better open rates |
| **Content variations** | Create multiple versions for A/B testing |
### Web Analytics Assistant
Answer questions about your website traffic with the [Web Analytics assistant](/web-analytics/ai-assistant):
| Feature | Description |
| - | - |
| **Ask about traffic** | Query sessions, pages, sources and goals in plain language |
| **Period summaries** | A written analysis of what moved, against the comparison period |
| **Drive the dashboard** | Change the period, apply filters, or open an Explore report |
## Enhanced with Firecrawl
The Anthropic AI agent can be enhanced by adding the [Firecrawl integration](/integrations/firecrawl). This gives the AI agent web access capabilities:
| Capability | Description |
| - | - |
| **Web research** | Search the web for current information and trends |
| **URL scraping** | Extract content from specific web pages for reference |
| **Competitive analysis** | Research competitor content and strategies |
| **Fact verification** | Verify claims against current web sources |
When Firecrawl is configured, the following tools become available to the AI agent:
| Tool | Description |
| - | - |
| `scrape_url` | Scrapes a URL and returns its content as markdown |
| `search_web` | Searches the web and returns relevant URLs |
## Prerequisites
Before configuring Anthropic:
1. **Anthropic Account**: Create an account at [console.anthropic.com](https://console.anthropic.com)
2. **API Key**: Generate an API key from the Anthropic console
3. **Credits**: Ensure your account has sufficient API credits
## Usage Notes
API calls to Claude count toward your Anthropic account usage. Monitor your usage in the Anthropic console to manage costs.
For most use cases, **Claude Sonnet 4.6** offers the best balance of capability and cost. Use **Opus 4.6** for complex creative tasks and **Haiku 4.5** for simple, high-volume operations.
# Firecrawl
Source: https://docs.notifuse.com/integrations/firecrawl
Configure Firecrawl to give the AI agent web access for research and content generation.
Firecrawl provides web scraping and search capabilities that enhance the AI agent's ability to access and process web content. When combined with the [Anthropic integration](/integrations/anthropic), it gives the AI agent web access for research and content generation.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Firecrawl**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Key** | Your Firecrawl API key | `fc-...` |
| **Base URL** | Custom API endpoint (optional) | `https://api.firecrawl.dev` |
Leave the Base URL blank to use the default Firecrawl API (`api.firecrawl.dev`). Only specify a custom URL if you're using a self-hosted Firecrawl instance.
## Available Tools
When Firecrawl is configured, the following tools become available to the AI agent:
| Tool | Description |
| - | - |
| `scrape_url` | Scrapes a URL and returns its content as clean markdown |
| `search_web` | Searches the web and returns relevant URLs with snippets |
## How It Enhances the AI Agent
When both Firecrawl and [Anthropic](/integrations/anthropic) integrations are configured, the AI agent gains powerful web capabilities:
### Web Research
| Capability | Description |
| - | - |
| **Search the web** | Find current information, news, and trends relevant to your content |
| **Extract content** | Pull text from specific URLs for reference or inspiration |
| **Competitive research** | Analyze competitor websites and content |
### Blog Content Creation
With web access, the AI can:
* Research topics thoroughly before writing
* Include up-to-date information and statistics
* Reference and link to authoritative sources
* Verify facts against current web content
### Email Marketing
Enhance email content with:
* Current industry trends and news
* Product information from your website
* Competitive positioning insights
## Prerequisites
Before configuring Firecrawl:
1. **Firecrawl Account**: Create an account at [firecrawl.dev](https://firecrawl.dev)
2. **API Key**: Generate an API key from the Firecrawl dashboard
3. **Credits**: Ensure your account has sufficient API credits
## Usage Notes
Each web search or URL scrape counts toward your Firecrawl API quota. Monitor your usage in the Firecrawl dashboard to manage costs.
The AI agent will automatically use these tools when web access is needed. For best results, ensure your Firecrawl plan has sufficient credits for your expected usage.
# Google Gemini
Source: https://docs.notifuse.com/integrations/gemini
Configure Google Gemini to power blog content creation and email generation in Notifuse.
The Google Gemini integration enables AI-powered features across Notifuse using Google's Gemini models. This integration powers the [AI assistant](/features/ai-assistant) for blog content creation, email design and web analytics.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Google Gemini**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Key** | Your Gemini API key (Gemini Developer API) | `AIza...` |
| **Model** | Pick a suggested model or type any Gemini model ID | `gemini-3.1-pro-preview` |
The **Model** field is a free-text input with suggestions, so you can enter any current Gemini model ID without waiting for a Notifuse update.
### Recommended Model
| Model | Description | \$ Pricing (per MTok) |
| - | - | - |
| **Gemini 3.1 Pro** | Most capable model — recommended for the AI agent's structured output | $2 input / $12 output |
The AI agent generates structured MJML email templates and full blog posts with HTML formatting. Only capable models such as **Gemini Pro** are smart enough to produce correct output. Lighter **Flash** / **Flash-Lite** models often generate broken templates or malformed content, so they are not recommended for the agent.
## Features Enabled
Once configured, the Google Gemini integration enables the following AI-powered features:
### Blog AI Agent
The AI agent can assist with blog content creation:
| Feature | Description |
| - | - |
| **Write blog posts** | Generate complete blog articles based on topics or brief descriptions |
| **SEO optimization** | Automatically generate SEO-friendly meta titles, descriptions, and keywords |
| **Content editing** | Improve, expand, or summarize existing content |
### Email Editor AI
Generate emails automatically using AI:
| Feature | Description |
| - | - |
| **Email generation** | Create email content from prompts or descriptions |
| **Subject lines** | Generate compelling subject lines for better open rates |
| **Content variations** | Create multiple versions for A/B testing |
### Web Analytics Assistant
Answer questions about your website traffic with the [Web Analytics assistant](/web-analytics/ai-assistant):
| Feature | Description |
| - | - |
| **Ask about traffic** | Query sessions, pages, sources and goals in plain language |
| **Period summaries** | A written analysis of what moved, against the comparison period |
| **Drive the dashboard** | Change the period, apply filters, or open an Explore report |
## Enhanced with Firecrawl
The Google Gemini AI agent can be enhanced by adding the [Firecrawl integration](/integrations/firecrawl). This gives the AI agent web access capabilities:
| Capability | Description |
| - | - |
| **Web research** | Search the web for current information and trends |
| **URL scraping** | Extract content from specific web pages for reference |
| **Competitive analysis** | Research competitor content and strategies |
| **Fact verification** | Verify claims against current web sources |
When Firecrawl is configured, the following tools become available to the AI agent:
| Tool | Description |
| - | - |
| `scrape_url` | Scrapes a URL and returns its content as markdown |
| `search_web` | Searches the web and returns relevant URLs |
## Prerequisites
Before configuring Google Gemini:
1. **Google AI Studio Account**: Create an account at [aistudio.google.com](https://aistudio.google.com)
2. **API Key**: Generate an API key from [Google AI Studio](https://aistudio.google.com/apikey)
3. **Quota / Billing**: Ensure your project has sufficient quota or billing enabled for the chosen model
## Usage Notes
API calls to Gemini count toward your Google AI usage. Monitor your usage in [Google AI Studio](https://aistudio.google.com) to manage costs.
The assistant panel shows a running cost for the Gemini models Notifuse has prices for. A model ID it does not recognise still works, but the conversation cost reads `$0.0000` — check Google AI Studio for what you actually spent.
Preview models such as `gemini-3.1-pro-preview` evolve over time. If a model ID is rejected, check the current list in the [Gemini API models documentation](https://ai.google.dev/gemini-api/docs/models) and enter it in the **Model** field.
# Mailgun
Source: https://docs.notifuse.com/integrations/mailgun
Configure Mailgun as your email provider in Notifuse.
Mailgun integration allows you to use Mailgun for sending transactional emails through Notifuse.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Mailgun**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **Domain** | Your verified sending domain | `mg.yourdomain.com` |
| **API Key** | Your Mailgun private API key | `key-1234567890abcdef...` |
| **Region** | Mailgun region dropdown | `US` or `EU` |
### Senders List
Add sender addresses that have been verified in your Mailgun account:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Support Team` |
| **Email** | Sender email address | `support@yourdomain.com` |
**Important**: Senders must be verified in Mailgun console before they can be used.
### Usage Types
Mailgun can be used for both transactional emails and broadcast campaigns:
| Option | Description | Examples |
| - | - | - |
| **Use for Transactional** | System-generated emails triggered by user actions | Password resets, order confirmations, account notifications |
| **Use for Broadcasts** | Mass email campaigns and newsletters | Marketing newsletters, product updates, promotional emails |
### Webhooks
Notifuse automatically registers the following Mailgun webhooks:
| Webhook Type | Description | Purpose |
| - | - | - |
| **Delivery** | Email successfully delivered | Track successful deliveries |
| **Bounce** | Email bounced | Monitor bounces |
| **Complaints** | Recipient filed abuse complaint | Handle abuse reports |
These webhooks provide real-time feedback on email performance and delivery status.
## Inbound replies (stop-on-reply)
To use **[Exit on reply](/features/automations#exit-on-reply)** in automations, Mailgun must forward inbound replies back to Notifuse. This is a one-time setup with two parts:
1. **Point your domain's MX records at Mailgun.** Add Mailgun's inbound MX records to your sending domain's DNS—typically `mxa.mailgun.org` and `mxb.mailgun.org`, both at priority `10`. Without these, replies never reach Mailgun. Mailgun has no API for DNS, so this step is manual.
2. **Register the inbound route.** Click **Register Webhooks** on the Mailgun integration (**Settings → Integrations**). Alongside the delivery, bounce, and complaint webhooks, Notifuse creates a Mailgun **Route** that forwards inbound mail for your domain to Notifuse. The route is non-preemptive—it forwards a copy and lets any other Mailgun routes you've configured keep running, so it won't disrupt an existing inbound setup.
Replies are matched to the original send by their `Message-ID` (an unguessable value Notifuse sets at send time), so no separate inbound signing key is required. The inbound endpoint is rate-limited.
Once both parts are in place, a contact's reply to an automation email will exit their journey whenever that automation has **Exit on reply** enabled. Bounces and out-of-office auto-replies are filtered out and never count as a reply.
## Prerequisites
Before configuring Mailgun:
1. **Mailgun Account**: Active Mailgun account
2. **Verified Domain**: Domain verified in Mailgun console
3. **API Key**: Mailgun private API key
## Testing
Click **Test Integration** to verify your configuration.
# Mailjet
Source: https://docs.notifuse.com/integrations/mailjet
Configure Mailjet as your email provider in Notifuse.
Mailjet integration allows you to use Mailjet for sending marketing emails through Notifuse.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Mailjet**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Key** | Mailjet public API key | `1234567890abcdef1234567890abcdef` |
| **Secret Key** | Mailjet private secret key | `abcdef1234567890abcdef1234567890` |
| **Sandbox Mode** | Enable sandbox mode toggle | `Disabled` (for production) |
### Senders List
Add sender addresses that have been verified in your Mailjet account:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Marketing Team` |
| **Email** | Sender email address | `marketing@yourdomain.com` |
**Important**: Senders must be verified in Mailjet console before they can be used.
### Usage Types
Mailjet can only be used for marketing emails:
| Option | Description | Examples |
| - | - | - |
| **Use for Marketing** | Promotional and newsletter emails sent to subscribers | Product announcements, newsletters, promotional campaigns |
### Webhooks
Notifuse automatically registers the following Mailjet webhooks:
| Webhook Type | Description | Purpose |
| - | - | - |
| **Delivery** | Email successfully delivered | Track successful deliveries |
| **Bounces** | Email bounced | Monitor bounces |
| **Complaints** | Recipient filed abuse complaint | Handle abuse reports |
These webhooks provide real-time feedback on email performance and delivery status.
## Prerequisites
Before configuring Mailjet:
1. **Mailjet Account**: Active Mailjet account
2. **Verified Senders**: Email addresses or domains verified in Mailjet console
3. **API Credentials**: Mailjet API key and secret key
## Testing
Click **Test Integration** to verify your configuration.
# OpenAI
Source: https://docs.notifuse.com/integrations/openai
Configure OpenAI to power blog content creation and email generation in Notifuse.
The OpenAI integration enables AI-powered features across Notifuse using GPT models. This integration powers the [AI assistant](/features/ai-assistant) for blog content creation, email design and web analytics.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **OpenAI**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Key** | Your OpenAI API key | `sk-proj-...` |
| **Model** | The model ID to use — free text, so any current or future model works | `gpt-4.1` |
| **Base URL** | Optional. The API endpoint, for OpenAI-compatible providers. Leave empty for OpenAI itself | `https://openrouter.ai/api/v1` |
| **Reasoning effort** | Optional. How much reasoning models think: None, Minimal, Low, Medium, High or Extra high | `Default` |
### Choosing a model
The Model field is free text rather than a fixed list, so you are never blocked from a model that shipped after your Notifuse version — type the ID exactly as your provider spells it.
| Model | Good for |
| - | - |
| **GPT-4.1** | The default. Reliable for MJML templates and long-form posts |
| **GPT-5.4** | Complex reasoning and the most demanding generations |
| **o4-mini** | Cheaper reasoning for shorter, simpler tasks |
See [OpenAI's pricing page](https://platform.openai.com/docs/pricing) for current rates.
Leave **Reasoning effort** on Default for non-reasoning models such as GPT-4.1. Providers that do not support the setting ignore it. If generations come back cut off, lowering the effort gives the visible answer more of the output budget.
The assistant panel shows a running cost for Anthropic and Gemini conversations, but not for OpenAI: with any model ID accepted — including every model on OpenRouter and any self-hosted runtime — Notifuse has no price list to compute it from. Track spend in your provider's dashboard.
## Features Enabled
Once configured, the OpenAI integration enables the following AI-powered features:
### Blog AI Agent
The AI agent can assist with blog content creation:
| Feature | Description |
| - | - |
| **Write blog posts** | Generate complete blog articles based on topics or brief descriptions |
| **SEO optimization** | Automatically generate SEO-friendly meta titles, descriptions, and keywords |
| **Content editing** | Improve, expand, or summarize existing content |
### Email Editor AI
Generate emails automatically using AI:
| Feature | Description |
| - | - |
| **Email generation** | Create email content from prompts or descriptions |
| **Subject lines** | Generate compelling subject lines for better open rates |
| **Content variations** | Create multiple versions for A/B testing |
### Web Analytics Assistant
Answer questions about your website traffic with the [Web Analytics assistant](/web-analytics/ai-assistant):
| Feature | Description |
| - | - |
| **Ask about traffic** | Query sessions, pages, sources and goals in plain language |
| **Period summaries** | A written analysis of what moved, against the comparison period |
| **Drive the dashboard** | Change the period, apply filters, or open an Explore report |
## Enhanced with Firecrawl
The OpenAI AI agent can be enhanced by adding the [Firecrawl integration](/integrations/firecrawl). This gives the AI agent web access capabilities:
| Capability | Description |
| - | - |
| **Web research** | Search the web for current information and trends |
| **URL scraping** | Extract content from specific web pages for reference |
| **Competitive analysis** | Research competitor content and strategies |
| **Fact verification** | Verify claims against current web sources |
When Firecrawl is configured, the following tools become available to the AI agent:
| Tool | Description |
| - | - |
| `scrape_url` | Scrapes a URL and returns its content as markdown |
| `search_web` | Searches the web and returns relevant URLs |
## Using OpenRouter
[OpenRouter](https://openrouter.ai) is a unified API gateway that gives you access to hundreds of AI models (Google Gemini, Meta Llama, Mistral, DeepSeek, and more) through a single OpenAI-compatible endpoint.
Since Notifuse's OpenAI integration uses the standard OpenAI SDK, you can point it to OpenRouter by setting the **Base URL** field:
### Form Fields for OpenRouter
| Field | Description | Value |
| - | - | - |
| **API Key** | Your OpenRouter API key | `sk-or-v1-...` |
| **Model** | Any model available on OpenRouter | `google/gemini-3-pro-preview` |
| **Base URL** | OpenRouter API base URL | `https://openrouter.ai/api/v1` |
This allows you to use any model supported by OpenRouter — including Google Gemini, Meta Llama, Mistral, DeepSeek, Qwen, and many others — directly from Notifuse's AI features.
The same field covers any other OpenAI-compatible endpoint: Azure OpenAI, or a self-hosted runtime such as Ollama, vLLM or LiteLLM.
Browse available models at [openrouter.ai/models](https://openrouter.ai/models) to find the best fit for your use case and budget.
The AI agent generates structured MJML email templates and full blog posts with HTML formatting. Only models with strong coding capabilities (e.g. GPT-4.1, Claude Sonnet, Gemini Pro) are smart enough to produce correct output. Smaller or weaker models may generate broken templates or malformed content.
## Prerequisites
Before configuring OpenAI:
1. **OpenAI Account**: Create an account at [platform.openai.com](https://platform.openai.com)
2. **API Key**: Generate an API key from the OpenAI dashboard
3. **Credits**: Ensure your account has sufficient API credits
## Usage Notes
API calls to GPT models count toward your OpenAI account usage. Monitor your usage in the OpenAI dashboard to manage costs.
For most use cases, **GPT-4.1** offers a good balance of capability and cost. Step up to a reasoning model for complex creative work, and down to a mini or nano variant for simple, high-volume operations.
# Postmark
Source: https://docs.notifuse.com/integrations/postmark
Configure Postmark as your email provider in Notifuse.
Postmark integration allows you to use Postmark for sending transactional emails through Notifuse.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **Postmark**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **Server Token** | Postmark server API token | `12345678-1234-1234-1234-123456789012` |
### Senders List
Add sender addresses that have been verified in your Postmark account:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Support Team` |
| **Email** | Sender email address | `support@yourdomain.com` |
**Important**: Senders must be verified in Postmark console before they can be used.
### Usage Types
Postmark can only be used for transactional emails:
| Option | Description | Examples |
| - | - | - |
| **Use for Transactional** | System-generated emails triggered by user actions | Password resets, order confirmations, account notifications |
### Webhooks
Notifuse automatically registers the following Postmark webhooks:
| Webhook Type | Description | Purpose |
| - | - | - |
| **Delivery** | Email successfully delivered | Track successful deliveries |
| **Bounce** | Email bounced | Monitor bounces |
| **Complaints** | Recipient filed abuse complaint | Handle abuse reports |
These webhooks provide real-time feedback on email performance and delivery status.
## Prerequisites
Before configuring Postmark:
1. **Postmark Account**: Active Postmark account
2. **Verified Senders**: Email addresses or domains verified in Postmark console
3. **Server Token**: Postmark server API token from your account
## Testing
Click **Test Integration** to verify your configuration.
# SendGrid
Source: https://docs.notifuse.com/integrations/sendgrid
Configure SendGrid as your email provider in Notifuse.
SendGrid integration allows you to use SendGrid for sending emails through Notifuse.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **SendGrid**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Key** | SendGrid API key with send permissions | `SG.xxxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` |
### Senders List
Add sender addresses that have been verified in your SendGrid account:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Support Team` |
| **Email** | Sender email address | `support@yourdomain.com` |
**Important**: Senders must be verified through SendGrid's Sender Authentication before they can be used.
### Usage Types
Configure how this SendGrid integration will be used:
| Option | Description | Examples |
| - | - | - |
| **Use for Transactional** | System-generated emails triggered by user actions | Password resets, order confirmations, account notifications |
| **Use for Marketing** | Promotional and newsletter emails sent to subscribers | Product announcements, newsletters, promotional campaigns |
You can enable both options to use the same SendGrid configuration for all email types.
### Webhooks
Notifuse automatically registers the following SendGrid webhooks:
| Webhook Type | Description | Purpose |
| - | - | - |
| **Delivered** | Email successfully delivered | Track successful deliveries |
| **Bounce** | Email bounced | Monitor bounces |
| **Dropped** | Email dropped before delivery | Track delivery failures |
| **Spam Report** | Recipient filed abuse complaint | Handle abuse reports |
| **Deferred** | Email delivery deferred | Track temporary delivery issues |
These webhooks provide real-time feedback on email performance and delivery status.
## Prerequisites
Before configuring SendGrid:
1. **SendGrid Account**: Active SendGrid account
2. **Verified Senders**: Email addresses or domains verified through SendGrid's Sender Authentication
3. **API Key**: SendGrid API key with **Mail Send** permissions (starts with `SG.`)
## Creating an API Key
1. Log in to your SendGrid account
2. Navigate to **Settings** → **API Keys**
3. Click **Create API Key**
4. Select **Restricted Access** and enable:
* **Mail Send** → Full Access
* **Tracking** → Full Access (for webhooks)
5. Copy the generated API key (it starts with `SG.`)
## Testing
Click **Test Integration** to verify your configuration.
# SMTP
Source: https://docs.notifuse.com/integrations/smtp
Configure SMTP as your email provider in Notifuse with Basic Auth or OAuth2.
SMTP integration allows you to connect any SMTP-compatible email service to Notifuse, supporting both traditional password authentication and modern OAuth2 for Microsoft 365 and Gmail.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **SMTP**.
### Basic Settings
| Field | Description | Example |
| - | - | - |
| **Integration Name** | Display name for this SMTP configuration | `Company SMTP` |
| **SMTP Host** | SMTP server hostname | `smtp.gmail.com` |
| **SMTP Port** | SMTP server port number | `587` |
| **Use TLS** | Enable TLS encryption (recommended) | `On` |
| **EHLO Hostname** | Custom hostname for the SMTP EHLO command. Defaults to the SMTP host value when empty. Useful when SMTP servers reject `EHLO localhost`. | `mail.yourdomain.com` |
### Authentication Type
Notifuse supports two authentication methods:
| Type | Description | Use Case |
| - | - | - |
| **Basic Auth** | Traditional username/password authentication | Most SMTP servers, app passwords |
| **OAuth2** | Modern token-based authentication | Microsoft 365, Gmail (required since 2024) |
***
## Basic Authentication
For traditional SMTP servers or services that support app passwords.
| Field | Description | Example |
| - | - | - |
| **SMTP Username** | Authentication username | `your-email@gmail.com` |
| **SMTP Password** | Authentication password | `your-app-password` |
***
## OAuth2 Authentication
OAuth2 is required for Microsoft 365 and Gmail since they have deprecated basic authentication.
### Microsoft 365 OAuth2
Microsoft 365 uses the **Client Credentials Flow** for server-to-server authentication.
#### Prerequisites
* Microsoft 365 Business account (not personal Outlook.com)
* Azure AD admin access
* Exchange Online admin access
#### Step 1: Register Azure AD Application
1. Go to [Azure Portal](https://portal.azure.com) → **Azure Active Directory** → **App registrations**
2. Click **New registration**
3. Enter a name (e.g., "Notifuse SMTP")
4. Select **Accounts in this organizational directory only** (Single tenant)
5. Click **Register**
6. Note the **Application (client) ID** and **Directory (tenant) ID**
#### Step 2: Create Client Secret
1. In your app registration, go to **Certificates & secrets**
2. Click **New client secret**
3. Add a description and select expiry period
4. Click **Add**
5. **Copy the secret value immediately** (it won't be shown again)
#### Step 3: Add API Permission
1. Go to **API permissions** → **Add a permission**
2. Select **APIs my organization uses**
3. Search for **Office 365 Exchange Online**
4. Select **Application permissions**
5. Check **SMTP.SendAsApp**
6. Click **Add permissions**
7. Click **Grant admin consent for \[Your Organization]**
#### Step 4: Register Service Principal in Exchange Online
This step is **critical** and often missed. Connect to Exchange Online PowerShell:
```powershell theme={null}
# Install module if needed
Install-Module -Name ExchangeOnlineManagement
# Connect to Exchange Online
Connect-ExchangeOnline -UserPrincipalName admin@yourdomain.com
# Register the service principal
New-ServicePrincipal -AppId -ObjectId
```
**Common Mistake:** The `ObjectId` must be from **Enterprise Applications**, NOT from App Registrations.
To find the correct Object ID:
1. Go to Azure Portal → **Microsoft Entra ID** → **Enterprise Applications**
2. Search for your app name
3. Copy the **Object ID** from the Overview page
#### Step 5: Grant Mailbox Permission
Grant the service principal permission to send as a specific mailbox:
```powershell theme={null}
# Grant full access to the mailbox
Add-MailboxPermission -Identity "sender@yourdomain.com" -User -AccessRights FullAccess
# Grant send-as permission
Add-RecipientPermission -Identity "sender@yourdomain.com" -Trustee -AccessRights SendAs
```
#### Step 6: Enable SMTP AUTH on the Mailbox
SMTP AUTH must be enabled on the sending mailbox. This can be done via Microsoft 365 Admin Center or PowerShell:
**Option A: Microsoft 365 Admin Center**
1. Go to **Users** → **Active users**
2. Select the sender user
3. Click **Mail** tab → **Manage email apps**
4. Check **Authenticated SMTP**
5. Click **Save changes**
**Option B: PowerShell**
```powershell theme={null}
# Enable SMTP AUTH for the mailbox
Set-CASMailbox -Identity "sender@yourdomain.com" -SmtpClientAuthenticationDisabled $false
```
If **Security Defaults** is enabled in your Microsoft Entra tenant, SMTP AUTH is blocked organization-wide. You may need to disable Security Defaults or use Conditional Access policies instead.
#### Step 7: Configure in Notifuse
| Field | Value |
| - | - |
| **SMTP Host** | `smtp.office365.com` |
| **SMTP Port** | `587` |
| **Use TLS** | `On` |
| **Auth Type** | `OAuth2` |
| **OAuth2 Provider** | `Microsoft` |
| **Username** | Sender email (e.g., `sender@yourdomain.com`) |
| **Tenant ID** | Your Azure AD Directory (tenant) ID |
| **Client ID** | Application (client) ID from Azure |
| **Client Secret** | Secret value from Step 2 |
The **Username** must match the mailbox you granted permissions to in Step 5.
***
### Gmail OAuth2
Gmail uses the **Refresh Token Flow** for authentication.
#### Prerequisites
* Google account (personal Gmail or Google Workspace)
* Google Cloud Console access
* Terminal with `curl` installed
#### Step 1: Create Google Cloud Project
1. Go to [Google Cloud Console](https://console.cloud.google.com)
2. Create a new project or select an existing one
3. Enable the **Gmail API**:
* Go to **APIs & Services** → **Library**
* Search for "Gmail API" and enable it
#### Step 2: Configure OAuth Consent Screen
1. Go to **APIs & Services** → **OAuth consent screen**
2. Select **External** user type (or Internal for Workspace)
3. Fill in app information:
* App name: "Notifuse" (or your choice)
* User support email: Your email
* Developer contact: Your email
4. Click **Save and Continue**
5. On the **Scopes** page, click **Add or Remove Scopes**
6. Add scope: `https://mail.google.com/`
7. Click **Save and Continue**
8. On **Test users** page, add your Gmail address
9. Click **Save and Continue** to complete the wizard
#### Step 3: Publish Your App (Required for Long-Lived Tokens)
**Critical**: Apps in "Testing" mode have refresh tokens that expire after **7 days**. To get permanent refresh tokens, you must publish your app.
1. Go to **APIs & Services** → **OAuth consent screen**
2. Click **Publish App**
3. Confirm by clicking **Confirm**
Your app status will change from "Testing" to "In production".
**No Google Verification Needed for Personal Use**
Publishing your app does NOT require Google verification if:
* You have fewer than 100 users
* You're using it for personal/internal purposes
You may see an "unverified app" warning when authorizing - simply click **Advanced** → **Go to \[App Name] (unsafe)** to proceed.
#### Step 4: Create OAuth Credentials
1. Go to **APIs & Services** → **Credentials**
2. Click **Create Credentials** → **OAuth client ID**
3. Select **Web application**
4. Name it (e.g., "Notifuse SMTP")
5. Under **Authorized redirect URIs**, click **Add URI**
6. Enter: `http://localhost`
7. Click **Create**
8. A dialog will show your **Client ID** and **Client Secret** - save both
#### Step 5: Get Authorization Code
Open this URL in your browser, replacing `YOUR_CLIENT_ID` with your actual Client ID:
```
https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost&scope=https://mail.google.com/&response_type=code&access_type=offline&prompt=consent
```
**Important URL parameters:**
* `scope=https://mail.google.com/` - Required for SMTP access
* `access_type=offline` - Required to receive a refresh token
* `prompt=consent` - Forces Google to return a new refresh token
After signing in and granting permission, your browser will redirect to something like:
```
http://localhost/?code=4/0AQSTgQF...LONG_CODE...&scope=https://mail.google.com/
```
Copy the entire `code` value (everything between `code=` and `&scope`).
If the code contains `%2F`, replace it with `/` before using it.
#### Step 6: Exchange Code for Refresh Token
Run this curl command in your terminal, replacing the placeholders:
```bash theme={null}
curl -X POST https://oauth2.googleapis.com/token \
-d "code=YOUR_AUTHORIZATION_CODE" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "redirect_uri=http://localhost" \
-d "grant_type=authorization_code"
```
Successful response:
```json theme={null}
{
"access_token": "ya29.a0AfH6SMBx...",
"expires_in": 3599,
"refresh_token": "1//0eHusbKw...",
"scope": "https://mail.google.com/",
"token_type": "Bearer"
}
```
**Save your `refresh_token` immediately!** Google only returns the refresh token on the first authorization. If you lose it, you'll need to revoke access and re-authorize.
#### Step 7: Configure in Notifuse
| Field | Value |
| - | - |
| **SMTP Host** | `smtp.gmail.com` |
| **SMTP Port** | `587` |
| **Use TLS** | `On` |
| **Auth Type** | `OAuth2` |
| **OAuth2 Provider** | `Google` |
| **Username** | Your Gmail address |
| **Client ID** | Client ID from Step 4 |
| **Client Secret** | Client Secret from Step 4 |
| **Refresh Token** | Refresh token from Step 6 |
#### Refresh Token Lifetime
With a **published app**, your refresh token will remain valid indefinitely, unless:
| Condition | Result |
| - | - |
| Token unused for **6 months** | Token expires |
| You change your **Google password** | Token is revoked |
| You exceed **50 refresh tokens** per client | Oldest token is automatically revoked |
| You manually revoke access at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) | Token is revoked |
If your token becomes invalid, repeat Steps 5-6 to generate a new refresh token.
***
## Senders List
Add sender addresses that can be used with this SMTP integration:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Support Team` |
| **Email** | Sender email address | `support@yourdomain.com` |
Click **Add Sender** to add multiple sender addresses.
***
## Usage Types
Configure how this SMTP integration will be used:
| Option | Description |
| - | - |
| **Use for Transactional** | Password resets, order confirmations, notifications |
| **Use for Marketing** | Newsletters, promotional campaigns, announcements |
You can enable both options to use the same SMTP configuration for all email types.
***
## Limitations
SMTP integrations have the following limitations:
* **No delivery webhooks**: SMTP does not provide real-time delivery status updates
* **No bounce notifications**: No automatic notifications when emails bounce
* **No complaint tracking**: No webhooks for spam reports or abuse complaints
Notifuse automatically adds **open and click tracking** to all emails, regardless of the email provider used.
For delivery webhooks and advanced features, consider using dedicated email service providers like [Mailgun](/integrations/mailgun), [Postmark](/integrations/postmark), or [SparkPost](/integrations/sparkpost).
***
## Common SMTP Servers
| Provider | Host | Port | TLS | Auth Type |
| - | - | - | - | - |
| Gmail | `smtp.gmail.com` | `587` | Yes | OAuth2 (required) |
| Microsoft 365 | `smtp.office365.com` | `587` | Yes | OAuth2 (required) |
| Amazon SES | `email-smtp.region.amazonaws.com` | `587` | Yes | Basic Auth |
| SendGrid | `smtp.sendgrid.net` | `587` | Yes | Basic Auth |
| Mailgun | `smtp.mailgun.org` | `587` | Yes | Basic Auth |
Gmail and Microsoft 365 have deprecated basic authentication. You must use OAuth2 for these providers.
***
## Troubleshooting
### Microsoft 365: "535 5.7.3 Authentication unsuccessful"
This error usually means:
1. Service principal not registered in Exchange Online (Step 4)
2. Mailbox permissions not granted (Step 5)
3. Username doesn't match the permitted mailbox
### Gmail: "unauthorized\_client"
This error means:
1. Refresh token was generated with different Client ID/Secret
2. Regenerate the refresh token using your current credentials
### Gmail: "invalid\_grant"
This error means:
1. Refresh token has expired (7 days in Testing mode)
2. User changed their Google password
3. Token was revoked or exceeded the 50-token limit
**Solution**: Generate a new refresh token following Step 4.
***
## Testing
Click **Test Integration** to send a test email and verify your configuration.
# SparkPost
Source: https://docs.notifuse.com/integrations/sparkpost
Configure SparkPost as your email provider in Notifuse.
SparkPost integration allows you to use SparkPost for sending emails through Notifuse.
## Configuration
Navigate to **Settings** → **Integrations** → **Add Integration** → **SparkPost**.
### Form Fields
| Field | Description | Example |
| - | - | - |
| **API Endpoint** | SparkPost API endpoint dropdown select | `https://api.eu.sparkpost.com` |
| **SparkPost API Key** | SparkPost API key with send permissions | `0123456789abcdef0123456789abcdef01234567` |
| **Sandbox Mode** | Enable sandbox mode toggle | `Disabled` (for production) |
### Senders List
Add sender addresses that have been verified in your SparkPost account:
| Field | Description | Example |
| - | - | - |
| **Name** | Display name for the sender | `Support Team` |
| **Email** | Sender email address | `support@yourdomain.com` |
**Important**: Senders must be verified in SparkPost console before they can be used.
### Usage Types
Configure how this SparkPost integration will be used:
| Option | Description | Examples |
| - | - | - |
| **Use for Transactional** | System-generated emails triggered by user actions | Password resets, order confirmations, account notifications |
| **Use for Marketing** | Promotional and newsletter emails sent to subscribers | Product announcements, newsletters, promotional campaigns |
You can enable both options to use the same SparkPost configuration for all email types.
### Webhooks
Notifuse automatically registers the following SparkPost webhooks:
| Webhook Type | Description | Purpose |
| - | - | - |
| **Delivery** | Email successfully delivered | Track successful deliveries |
| **Bounce** | Email bounced | Monitor bounces |
| **Complaints** | Recipient filed abuse complaint | Handle abuse reports |
These webhooks provide real-time feedback on email performance and delivery status.
## Prerequisites
Before configuring SparkPost:
1. **SparkPost Account**: Active SparkPost account
2. **Verified Senders**: Email addresses or domains verified in SparkPost console
3. **API Key**: SparkPost API key with send permissions
## Testing
Click **Test Integration** to verify your configuration.
# Supabase
Source: https://docs.notifuse.com/integrations/supabase
Integrate Supabase Auth with Notifuse for authentication emails and user sync.
Notifuse provides a seamless integration with Supabase Auth to automatically send authentication emails and sync user data with your contact list.
## Overview
The Supabase integration offers two main features:
1. **Auth Email Hook**: Automatically send transactional emails for authentication events (signup, magic link, password recovery, etc.)
2. **Before User Created Hook**: Sync newly created Supabase users to your Notifuse contact database
## Configuration
Navigate to Notifuse workspace **Settings** → **Integrations** → **Add Integration** → **Supabase**.
### Prerequisites
Before configuring the Supabase integration:
* A Notifuse workspace with appropriate permissions
* A Supabase project
* At least one email integration configured in Notifuse (for sending transactional emails)
## Auth Email Hook
The Auth Email Hook allows you to send branded, customized authentication emails through Notifuse instead of the default Supabase emails.
### Setup
1. Go to [Supabase Auth Hooks](https://supabase.com/dashboard/project/_/auth/hooks) in your Supabase Dashboard and open the **Auth Email Hook** settings
2. Add new Hook
3. Select "HTTPS" type
4. Click "Generate a secret" and copy its value to the Notifuse integration
5. Save the integration to see the webhook endpoint generated by Notifuse
6. Copy and past the webhook endpoint value to your Supabase hook and save
### Default Templates
When you first save the Supabase integration, Notifuse will automatically create default templates for all email types. These templates:
* Are marked as integration-managed (cannot be deleted)
* Include all necessary Liquid template variables from Supabase
* Can be customized to match your brand
### Template Variables
All Supabase auth email templates have access to these variables:
```liquid theme={null}
{# User information #}
{{ user.id }}
{{ user.email }}
{{ user.phone }}
{{ user.email_new }} {# Only for email change #}
{# Email data #}
{{ email_data.token }} {# OTP code (for display, not URLs) #}
{{ email_data.token_hash }} {# Hashed token for verification URLs #}
{{ email_data.token_new }} {# For secure email changes #}
{{ email_data.token_hash_new }} {# For secure email changes #}
{{ email_data.redirect_to }} {# Where to redirect after verification #}
{{ email_data.site_url }} {# Your Supabase auth URL #}
{{ email_data.email_action_type }} {# Type: signup, recovery, magiclink, etc. #}
```
### Confirmation URL Format
**Important:** All confirmation links must use this exact format to work with Supabase's GoTrue auth system:
```liquid theme={null}
{{ email_data.site_url }}/verify?token={{ email_data.token_hash }}&type={{ email_data.email_action_type }}&redirect_to={{ email_data.redirect_to }}
```
This constructs a URL like:
```
https://yourproject.supabase.co/auth/v1/verify?token=abc123...&type=signup&redirect_to=https://yourapp.com
```
**Note:** Use `token_hash` for the URL, not `token`. The `token` variable is for displaying OTP codes in reauthentication emails.
### Example Template
Here's an example signup confirmation template with the correct URL format:
```mjml theme={null}
Welcome!
Please confirm your email address by clicking the link below:
Confirm Email
If you did not sign up for this account, you can ignore this email.
```
## Before User Created Hook
The Before User Created Hook automatically adds new Supabase users to your Notifuse contact database.
### Setup
1. Toggle on **Before User Created Hook**
2. Generate a webhook secret key in Supabase (different from the auth email hook)
3. Copy the secret key in the Notifuse Supabase integration form
4. (Optional) Select a contact list to automatically add new users to
5. (Optional) Reject disposable email addresses (e.g: @yopmail.com)
### Field Mapping
Supabase user fields are automatically mapped to Notifuse contact fields:
| Supabase Field | Notifuse Field |
| - | - |
| `email` | `email` |
| `id` | `external_id` |
| `phone` | `phone` |
| `user_metadata` | Configurable Custom JSON field (optional) |
You can optionally choose which Custom JSON field (1-5) will receive the Supabase `user_metadata` in the integration settings. If no field is selected, the `user_metadata` will not be synced to Notifuse.
### Error Handling
The Before User Created Hook is configured to be lenient:
* If there's an error upserting the contact, the error is logged but the signup proceeds
* If the configured contact list is deleted, the error is logged but the signup proceeds
* This ensures that Notifuse issues never prevent user signups in your application
## Security
### Webhook Signature Verification
All webhooks from Supabase are verified using HMAC-SHA256 signatures. Notifuse will reject any webhooks with invalid signatures, ensuring that only authentic requests from your Supabase project are processed.
### Encryption
Webhook signature keys are encrypted at rest in Notifuse's database using AES-256 encryption.
## Testing
### Test Auth Emails
1. In your application, trigger an authentication flow (e.g., signup)
2. Check your Notifuse logs to confirm the webhook was received
3. Check the recipient's inbox for the email
### Test User Sync
1. Create a new user in your Supabase project
2. In Notifuse, navigate to **Contacts**
3. Search for the user's email to confirm they were added
4. If you configured a target list, verify the user was added to that list
## Troubleshooting
### Emails Not Sending
**Check these items:**
* Is the Auth Email Hook enabled in Notifuse?
* Is the webhook signature key correct in both Supabase and Notifuse?
* Do you have a transactional email provider configured in your workspace?
* Check the Notifuse logs for error messages
### Users Not Syncing
**Check these items:**
* Is the Before User Created Hook enabled in Notifuse?
* Is the webhook signature key correct in both Supabase and Notifuse?
* Check the Notifuse logs for error messages
* Remember: errors in the Before User Created Hook are logged but don't prevent signups
### Invalid Signature Errors
**This usually means:**
* The signature key in Notifuse doesn't match the one in Supabase
* The webhook URL includes the wrong `workspace_id` or `integration_id` parameters
* You're testing with a tool like Postman without the correct signature header
**To fix:**
* Regenerate the signature key in Supabase
* Copy the new key to Notifuse
* Ensure you're using the exact webhook URL provided by Notifuse
## Email Change Behavior
Supabase has a special "secure email change" flow that sends two emails:
1. **To the current email address**: Uses `template_mappings.email_change_current`
* Available variables: `user.email`, `user.email_new`, `email_data.token_hash_new`
* Confirmation URL: `{{ email_data.site_url }}/verify?token={{ email_data.token_hash_new }}&type=email_change&redirect_to={{ email_data.redirect_to }}`
2. **To the new email address**: Uses `template_mappings.email_change_new`
* Available variables: `user.email`, `user.email_new`, `email_data.token_hash`
* Confirmation URL: `{{ email_data.site_url }}/verify?token={{ email_data.token_hash }}&type=email_change&redirect_to={{ email_data.redirect_to }}`
**Note:** Supabase's token naming is counterintuitive for backward compatibility:
* `token_hash_new` is used for the current email address
* `token_hash` is used for the new email address
Both emails must be confirmed for the email change to complete. Notifuse automatically detects secure email changes and sends to the appropriate email addresses with the correct tokens.
## Advanced Configuration
### Custom Contact List Logic
If you need more sophisticated logic for which list to add users to (based on user metadata, for example), you can:
1. Leave the target list empty in the integration settings
2. Use Notifuse's segment feature to automatically organize users
3. Create an automation that triggers when new contacts are added
### Email Customization
All integration-managed templates can be edited to match your brand:
* Customize colors, fonts, and layout
* Add your logo and brand assets
* Modify copy while preserving template variables
* Add UTM parameters for tracking
**Note:** Integration-managed templates cannot be deleted, but they can be fully customized.
## Limitations
* Integration-managed templates cannot be deleted (they can be edited)
* Each Supabase project requires its own integration in Notifuse
* The Before User Created Hook only fires for new user signups (not for existing users)
* Template variables are determined by Supabase and cannot be modified
## Best Practices
1. **Test before going live**: Set up the integration in a development workspace first
2. **Customize templates**: The default templates are a starting point - customize them to match your brand
3. **Monitor logs**: Check Notifuse logs regularly to ensure webhooks are being processed
4. **Use segments**: Instead of static lists, use Notifuse segments to automatically organize users
5. **Keep keys secure**: Treat webhook signature keys like passwords - never commit them to version control
## Related Documentation
* [Supabase Auth Hooks: Send Email Hook](https://supabase.com/docs/guides/auth/auth-hooks/send-email-hook)
* [Supabase Auth Hooks: Before User Created Hook](https://supabase.com/docs/guides/auth/auth-hooks/before-user-created-hook)
* [Notifuse Email Templates](/concepts/templates)
* [Notifuse Contacts](/concepts/contacts)
* [Notifuse Lists](/concepts/lists)
# Zapier
Source: https://docs.notifuse.com/integrations/zapier
Connect Notifuse to Zapier: send your transactional emails from any app, trigger Zaps from contact, list and segment events, and add or subscribe contacts.
Zapier connects Notifuse to thousands of other applications without writing any code. Notifuse offers both halves of a Zap:
* **Triggers** start a Zap the moment something happens in Notifuse — a contact is created or updated, someone subscribes to or unsubscribes from a list, a contact enters or leaves a segment. They are real-time, not polled: Notifuse pushes each event to Zapier as it happens.
* **Actions** let another app reach into Notifuse. The one most people come for is **sending**: a Zap picks one of your transactional notifications and Notifuse sends it, using your template, your sender and your email provider. Actions can also create or update a contact, or subscribe a contact to a list.
## Before You Start
You need two things from Notifuse — an **API key** and your **API URL** — and connecting Zapier under **Settings** → **Integrations** hands you both.
You also need **HTTPS**. Zapier only talks to endpoints served over HTTPS with a certificate issued by a public certificate authority, so a self-hosted instance has to be reachable from the public internet.
## Step 1 — Connect Zapier
Open **Settings** → **Integrations** in the workspace you want Zapier to work in. Zapier sits at the bottom of the list of available connectors; on a workspace that already has an integration of any kind, that list is not shown and Zapier is under the **Add Integration** button instead. Either way, choose **Zapier**. Only the workspace owner can connect it.
You are asked for one thing: a **label**. It names the connection in Notifuse, and nothing else depends on what you type — a workspace can hold several Zapier connections, so use the label to say what each one is for. "Zapier" is filled in for you and is fine if you only need one.
Click **Connect Zapier**. Notifuse mints an API key with exactly the permissions this integration needs and nothing else: **Transactional**, **Webhook Subscriptions**, **Contacts** and **Lists** read and write, plus **Segments** read. There is nothing to tick — the grant is chosen for you.
Take **Transactional** seriously before you connect. It is what lets a Zap send, and it reaches further than that: it also permits creating, editing and deleting transactional notifications, testing templates and providers, and relaying through the SMTP bridge. A leaked token could send mail through your provider, at your cost and against your sending reputation. It cannot read your message history. If a Zapier connection has no business sending, narrow the key to suit — see *Managing the Connection Afterwards*.
The token is displayed **once**, in the panel that replaces the form. Copy it before you press **Done** — it cannot be retrieved afterwards, and no screen in Notifuse can show it to you again. The same panel prints the **API URL** you will need in Step 2, with a copy button beside it.
**The key's address is derived from your label, not typed by you.** Notifuse builds it as `zapier-
**Or create the key by hand.** **Settings** → **Team** → **Create API Key** works too, and it is where you grant the permissions yourself — see the table below for what to tick. A key built that way is an ordinary API key: it is attached to no connection, so no card appears under **Settings** → **Integrations**, and removing its row from **Settings** → **Team** is the only thing that revokes it. Give Zapier its own key rather than reusing an existing one. See [Admins & API Keys](/features/admins-api-keys) for the full permission reference.
### What the Key Can Do
This is a reference, not a checklist: connecting Zapier grants all of it for you. Read it to know what the token in Zapier's hands can reach — or to know what to tick if you are building the key by hand from **Settings** → **Team**, or narrowing one afterwards.
| What you want to use | Permissions the key needs |
| - | - |
| Connecting the account, the workspace picker, and the custom field labels in an action | None — any key can do these |
| Any trigger (turning a Zap on and off) | **Webhook Subscriptions** read and write |
| Any trigger's sample data — what Zapier shows you while you build a Zap | **Contacts** read |
| Contact triggers | **Contacts** read |
| List triggers, and the list picker | **Lists** read, and **Contacts** read |
| Segment triggers, and the segment picker | **Segments** read, and **Contacts** read |
| The **Send Transactional Email** action, and the notification picker | **Transactional** read **and** write |
| Contact fields filled in on a send being saved to the contact | **Contacts** write — without it the send still works, but only the address is recorded |
| CC or BCC on a send | **Contacts** read as well — the subject is rendered against the contact |
| The **Create or Update Contact** action | **Contacts** write |
| The **Subscribe Contact to List** action | **Lists** read **and** write |
**Read is what fills the dropdowns; write is what performs the step.** Every picker in this integration reads the things you choose from, so an action that only writes still cannot be configured. A key with **Lists** write but not **Lists** read makes the **Subscribe Contact to List** step impossible to save: its list picker answers with a permission error, and the list is a required field.
**Contacts** read is needed by every trigger too, not only the contact ones: the sample records Zapier shows while you build a Zap are read from your contacts whichever trigger you picked. A key without it fills its dropdowns but fails on **Test trigger**.
**Transactional write, Webhook Subscriptions write and Lists write all reach further than their names suggest.** Transactional write sends mail through your provider — and also permits editing and deleting your transactional notifications, testing providers, and the SMTP bridge. A trigger subscribes to events and has their payloads delivered to Zapier. Subscribing an address to a list creates or overwrites that contact's record, and so does sending to it. Treat this key as you would treat both your contact database and your sending credentials.
## Step 2 — Your API URL
Zapier's connection form has an **API URL** field, and the **Connect Zapier** panel prints the value to put in it — both before you connect and beside the token afterwards.
* **Notifuse Cloud**: the field arrives pre-filled with the Cloud API, `https://v3.notifuse.com`. Leave it as it is.
* **Self-hosted**: paste the address the panel prints — the same address you open the Notifuse console at, scheme and domain only, with nothing after the domain and no trailing slash. Do not clear the field: an empty API URL means Notifuse Cloud, not "this instance".
Your instance must be reachable from the public internet, over HTTPS, with a certificate from a public certificate authority. Let's Encrypt is fine.
**Plain `http://`, self-signed certificates and internal certificate authorities are not supported**, and neither is an instance reachable only from inside your network. Zapier requires HTTPS with a publicly trusted certificate. If your Notifuse instance sits behind a firewall, note that Zapier only publishes a fixed set of outbound IP addresses to allowlist on its paid plans.
## Step 3 — Connect Your Workspace
In Zapier, create a Zap, search for **Notifuse**, and choose a trigger or action. When Zapier asks you to connect an account, paste the API key and — if you self-host — your API URL.
Every Notifuse trigger and action then asks which **workspace** to use, from a dropdown.
**A Zapier connection covers one workspace.** A Notifuse API key belongs to exactly one workspace, so the workspace dropdown will only ever offer that one and selects it for you. If you run several workspaces, connect Zapier from **Settings** → **Integrations** in each one and add a separate Zapier connection for each — Zapier labels connections with the workspace name and the host they live on, so they are easy to tell apart.
## Managing the Connection Afterwards
The connection appears as a card under **Settings** → **Integrations**, showing your label, the address of the key it minted, and when it was connected. Two things you can do to it, and they are not equivalent:
* **Renaming it** changes the label on the card and nothing else. The key keeps the address it was minted under, so the card's name and the address can disagree afterwards — the card shows both for exactly that reason. Nothing has to be re-entered in Zapier.
* **Deleting it revokes the key.** Every Zap using that key stops working the moment you confirm, and there is no way back: a revoked key cannot be restored, and reconnecting mints a different one you must paste into Zapier. Deleting the workspace revokes its Zapier keys the same way.
**Settings** → **Team** is where a key's permissions change. Widen or narrow the Zapier key there — the change takes effect on its next request and **the token does not change**, so there is nothing to re-enter in Zapier. Removing the key's row from Team also revokes it, exactly as deleting the card does, but leaves the card behind pointing at a key that no longer exists.
## Triggers
| Trigger | Fires when |
| - | - |
| **New Contact** | A contact is added to the workspace |
| **Updated Contact** | An existing contact's details change |
| **New List Subscriber** | A contact becomes an active subscriber of a list |
| **Contact Unsubscribed from List** | A contact unsubscribes from a list |
| **Contact Joined Segment** | A contact starts matching a segment |
| **Contact Left Segment** | A contact stops matching a segment |
List and segment triggers let you pick a specific list or segment. Do pick one: Notifuse then sends Zapier only the events for that list or segment instead of every list in the workspace, which keeps your Zap's task usage down and your instance faster.
### New List Subscriber Covers Three Ways In
A contact can become an active subscriber of a list in more than one way, and only the first one is a plain new subscription:
* They **subscribe** for the first time.
* They **confirm** a double opt-in subscription that was waiting for confirmation.
* They **resubscribe** after previously unsubscribing, bouncing or reporting spam.
**New List Subscriber fires for all three**, because a returning contact never produces a first-time subscription event, and a Zap that only watched for one would silently miss every returning subscriber. Each run carries the specific event so you can branch on it inside Zapier if you need to treat a first-time subscriber differently from a returning one.
### Updated Contact Ignores No-Op Saves
**Updated Contact** fires only when a contact's details actually change. Saving a contact without changing anything, or a background job touching a record, produces no event. This is deliberate — otherwise routine internal updates would run your Zaps.
### Zapier Manages Its Own Webhook Subscriptions
Turning a Notifuse Zap on creates a subscription under **Settings** → **Webhooks**, marked as created by Zapier. Turning the Zap off removes it.
**Do not delete a Zapier-created webhook subscription by hand.** The Zap that owns it will stop receiving events, and it will not recreate the subscription on its own — you have to turn the Zap off and on again in Zapier, which works even though the subscription is already gone. Delete the Zap instead, and let it clean up after itself.
A delivery that fails is retried, so a Zap can occasionally run twice for the same event. If your Zap does something that must not happen twice — charging a card, sending a message — add a Zapier filter or a deduplication step.
## Actions
| Action | What it does |
| - | - |
| **Send Transactional Email** | Sends one of your transactional notifications to a contact, filling its template variables from the Zap |
| **Create or Update Contact** | Creates the contact if the email address is new, updates it otherwise |
| **Subscribe Contact to List** | Creates or updates the contact **and** subscribes it to the lists you pick, in one step |
All three actions match on the **email address**, so re-running them with the same address updates the same contact rather than creating a duplicate.
Notifuse's custom contact fields appear in these actions under the labels you gave them. Name your custom fields in **Settings** → **Custom Fields** first — unlabelled slots are hidden, so a field you cannot find in Zapier is usually a field with no label yet.
### Sending Is About Choosing, Not Composing
**Send Transactional Email** does not carry your email. The template, the sender, the tracking and the unsubscribe handling all stay in Notifuse — the Zap chooses which notification to send, who receives it, and the values its template renders with.
So the shape of the step is: pick a **Notification** from the dropdown, map an **Email** for the recipient, and fill **Template Data** with one row per `{{ variable }}` your template uses. A variable you do not supply renders empty rather than failing, so if an email arrives with a gap in it, that row is the first place to look — check the spelling against the template.
Two details about Template Data are worth knowing before you debug something strange:
* **Values arrive as text.** A template that prints a value is fine. A template that compares one — `{% if total > 100 %}` — will not match, because it is comparing against the text `"120"` rather than the number.
* **`contact` and `notification_center_url` are set by Notifuse** and cannot be overridden from the Zap. Rows with those names are ignored.
Notifications with no email channel configured are not offered in the dropdown at all, because sending one could only fail.
### Sending Also Writes the Contact
A send creates the contact if the address is new. That is usually what you want — you can email someone the first time they order without a separate step.
The part that surprises people is that **any contact field you fill in overwrites what is stored**. If you map First Name from a source that is sometimes blank, you will blank the stored first name on those runs. Leave a field empty in the Zap and Notifuse leaves the stored value alone; map a field and its value wins. If you only want to send, fill in nothing but the email address.
### Use Message External ID for Anything That Must Not Be Sent Twice
Zapier retries a step that failed, and a retry runs the whole action again. For a shipping notification that is a minor annoyance; for a password reset or an order confirmation it is a real problem.
Fill **Message External ID** with something stable from the trigger — an order number, an invoice id, the source record's id. Sending again with a value Notifuse has already seen returns the original message instead of sending a second email. Leave it blank and every run sends.
One limitation to be aware of: the step cannot tell you which of the two happened. A deduplicated send and a fresh send look identical in Zap History, both succeeding with the same message id.
**Testing the step sends a real email.** Zapier's **Test action** button performs a genuine send, not a dry run — as does every live run. Point it at an address you own while you are building the Zap.
### Subscription Status Is Not Always Active
This is the behaviour most likely to look like a bug when it is not. **Subscribe Contact to List** returns the resulting status for every list, and it is not always `active`:
* **A contact who previously unsubscribed comes back as `pending`, not `active`**, and receives the list's confirmation email. Notifuse forces double opt-in for anyone who has opted out before, whatever the list's own settings say. This is a compliance rule and it cannot be switched off. The Zap did not fail — the contact simply has to confirm before they receive anything. Note that a list with no double opt-in template configured has no confirmation email to send, so those contacts stay `pending` indefinitely: set one up before pointing a re-engagement Zap at a list.
* **A brand-new contact lands `active` even on a double opt-in list**, and receives no confirmation email, because the request came from an authenticated API key rather than from a public subscription form.
Read those two together before you point a lead-capture Zap at a double opt-in list: new addresses skip the confirmation step. Only add people to a list through Zapier when you have their consent, exactly as you would when importing a file.
Three more cases where the action reports success without adding a new subscriber. They do not all look the same in the output, which matters if you filter on the result:
* **An address that has bounced or reported spam is not resubscribed**, deliberately — it comes back reported with its terminal status, `bounced` or `complained`. A Filter step keyed on **Status** sees it.
* **A contact already active on the list is left alone.** Re-running the Zap changes nothing, and the membership comes back as `active`.
* **An address at a disposable email domain is dropped silently.** This is the only case with nothing to report: the step succeeds with **no memberships at all** and a blank **Status**. Nothing was written — not the membership, and not the contact.
**Filter on the status value, not on the status existing.** *Continue if Status exists* passes a bounced address just as happily as an active one — both carry a status — and stops only the disposable-domain case. A filter that means "the contact is now subscribed" should check that **Status** is `active`, or that **Memberships** is not empty if you are subscribing to several lists at once.
## Troubleshooting
### The connection test fails
* Check the API URL: it must start with `https://` and contain no path after the domain. The connect panel printed the exact value, and reopening the card only offers the label — so if you no longer have it, take the scheme and domain of the address you open the Notifuse console at and drop everything after the domain.
* Open `https://your-domain/api/workspaces.list` in a browser. An authentication error is the expected answer and proves the address is reachable; a timeout, a connection error or a certificate warning means Zapier cannot reach it either.
* Check the certificate is issued by a public certificate authority, not a self-signed or internal one.
* If the key was revoked in Notifuse — by deleting the Zapier connection, or by removing its row from **Settings** → **Team** — the connection stops working immediately. Zapier reports it as an authentication failure and offers to reconnect the account: connect Zapier again and paste the new token in.
### A step fails with a permission error
The message names the missing permission. Add it to the key from **Settings** → **Team**; the change takes effect on the next request and the token stays the same. Do not connect Zapier a second time to fix this — a fresh connection mints a key with the same standard grant, and the step fails in the same place.
One case has a known cause. If **Send Transactional Email** fails on **Transactional**, or its notification picker is empty and reports a permission error, the connection predates Notifuse 39.1 — the release that added sending to the standard grant. Tick **Transactional** read and write on the key in **Settings** → **Team** and the step works on its next run, with no reconnection and no change to the token.
### A Zap stopped receiving events
Open **Settings** → **Webhooks** in Notifuse and look at the subscription for that Zap:
* **Switched off with a reason attached.** Notifuse disables a subscription automatically after a long run of failed deliveries and records why. Turn it back on once the cause is resolved — or, easier, turn the Zap off and on again in Zapier to recreate it.
* **Missing entirely.** Someone deleted it by hand, or the Zap was turned off. Turn the Zap off and on again.
The delivery log on each subscription shows every event sent, the response Zapier returned, and the payload — the fastest way to tell whether Notifuse sent an event at all.
### The trigger test finds no data
Zapier looks for recent matching activity to build its sample. If a list has had no subscribers recently, or a segment no new members, there is nothing to show. Create a test contact, subscribe it to the list, and re-run the test.
## Limitations
* **A connection covers one workspace.** Several workspaces means several connections.
* **Contact deletions do not trigger a Zap**, and neither do broadcast or automation events.
* **There is no search step yet** — you cannot look a contact up mid-Zap without an action that writes.
* **Custom fields must be labelled** to appear in an action.
* **A send cannot carry attachments.** The API accepts them, but Zapier hands a step a reference to a file rather than the file itself, so there is nothing to attach.
* **A send cannot compose an email.** It sends a notification you configured in Notifuse; it cannot supply its own body. You can override the subject, preview text, sender name and reply-to for one message.
## Related Documentation
* [Admins & API Keys](/features/admins-api-keys) — what each permission allows
* [Webhooks](/features/webhooks) — the event types and payloads behind the triggers
* [Contacts](/features/contacts)
* [Lists](/features/lists)
* [Segments](/features/contacts#contact-segmentation-engine)
# Self-hosted Installation
Source: https://docs.notifuse.com/self-hosting/installation
How to self-host Notifuse
**Looking for a managed solution?** [Notifuse Cloud](https://cloud.notifuse.com) takes care of hosting, security, backups, updates, and maintenance for you — starting from **\$16/month**.
## Getting started
Notifuse includes an interactive **Setup Wizard** that makes installation easy. Many environment variables are optional and can be configured through the web interface on first launch.
### Quick Start with Setup Wizard
1. Deploy Notifuse using one of the options below
2. Access your instance in a web browser
3. Complete the Setup Wizard:
* Enter your root administrator email
* Configure your API endpoint
* Set up SMTP settings
4. Start using Notifuse!
### Option 1: Docker Compose (Recommended for testing)
This option includes an embedded PostgreSQL database for easy testing and development:
```bash theme={null}
# Clone the repository (the compose.yaml builds from the included Dockerfile)
git clone https://github.com/notifuse/notifuse.git
cd notifuse
# Start Notifuse with embedded PostgreSQL
docker compose up -d
```
This will start Notifuse on port `8081` with a PostgreSQL database.
On first launch, you'll be guided through the Setup Wizard to configure your instance. Alternatively, you can configure [environment variables](#environment-variables) in a `.env` file or directly in the `compose.yaml`.
### Option 2: Standalone Docker (Production)
For production deployments, use the standalone Docker image with your own PostgreSQL database:
```bash theme={null}
docker run -d --name notifuse -p 8080:8080 notifuse/notifuse:latest
```
On first launch, you'll be guided through the Setup Wizard to configure your instance. Alternatively, you can configure [environment variables](#environment-variables).
**Note**: You'll need to provide your own PostgreSQL database.
### PostgreSQL database (Option 2 only)
If using the standalone Docker option, you can use any PostgreSQL database **with root credentials**. Notifuse automatically creates a system database for itself.
**Recommended version**: PostgreSQL 17 or higher
A new database will be created for each Notifuse workspace to avoid multi-tenant issues (that's why you need root credentials).
**Note**: This is not required when using Docker Compose as PostgreSQL is included.
### An SMTP server
Notifuse needs an SMTP server to send system emails (e.g. password reset emails, invitation emails, etc.).
If using SES, you can create SMTP credentials in the SMTP settings section of the SES dashboard.
### A public API endpoint
Notifuse needs a public API endpoint to be accessible from the web. Example: `https://emails.yourcompany.com`
## Environment Variables
With the Setup Wizard, many environment variables are optional and can be configured through the web interface. Environment variables always take precedence over database settings when present.
**Special Characters in .env Files**
If you're using a `.env` file and any of your values contain the `#` character (common in passwords), you **must** wrap the value in quotes:
```bash theme={null}
# ✅ CORRECT - value is quoted
DB_PASSWORD="mypass#word123"
SECRET_KEY="abc123#xyz789"
# ❌ INCORRECT - will be truncated at the # character
DB_PASSWORD=mypass#word123 # parsed as "mypass"
SECRET_KEY=abc123#xyz789 # parsed as "abc123"
```
This limitation only applies to `.env` files. Environment variables set directly in your shell, Docker Compose, or container orchestration platform do not have this restriction.
### Required Variables
| Variable | Description |
| - | - |
| `DB_HOST` | PostgreSQL host (e.g., `localhost` or `db.yourcompany.com`) |
| `DB_PORT` | PostgreSQL port (e.g., `5432`) |
| `DB_USER` | Database username (e.g., `postgres`) |
| `DB_PASSWORD` | Database password (e.g., `postgres`) |
| `SECRET_KEY` | Secret key for encryption (generate with command below) |
**Generate SECRET\_KEY:**
```bash theme={null}
openssl rand -base64 32
```
**IMPORTANT**: Never change your `SECRET_KEY` after initial setup. It encrypts all workspace
integration secrets (email provider API keys, SMTP passwords, etc.). Changing it will permanently
destroy all encrypted credentials.
### Application Variables (Optional with Setup Wizard)
These variables can be configured via the Setup Wizard on first launch, or set as environment variables. Environment variables always override wizard settings.
| Variable | Description |
| - | - |
| `ROOT_EMAIL` | Root administrator email (e.g., `admin@yourcompany.com`). Accepts multiple comma/semicolon-separated emails to grant root to several administrators (e.g., `alice@yourcompany.com,bob@yourcompany.com`); matching is case-sensitive. |
| `API_ENDPOINT` | Public API endpoint URL (e.g., `https://emails.yourcompany.com`) |
| `SMTP_HOST` | SMTP server host (e.g., `smtp.gmail.com`) |
| `SMTP_PORT` | SMTP server port (e.g., `587` or `465`) |
| `SMTP_USERNAME` | SMTP username (e.g., `noreply@yourcompany.com`) |
| `SMTP_PASSWORD` | SMTP password (e.g., `your_smtp_password`) |
| `SMTP_FROM_EMAIL` | From email address (e.g., `noreply@yourcompany.com`) |
| `SMTP_FROM_NAME` | From name (e.g., `Your Company Name`) |
| `SMTP_EHLO_HOSTNAME` | Custom EHLO hostname for SMTP connections (e.g., `mail.yourdomain.com`). Defaults to SMTP host value when empty. Useful when SMTP servers reject `EHLO localhost`. |
**Multiple root users**: `ROOT_EMAIL` can hold more than one email
(comma/semicolon-separated), so a shared instance can have several
administrators with root privileges — creating workspaces, accessing System
Settings, and authenticating programmatically. A user account is created for
each listed email on startup, so every root can sign in immediately. Matching
is case-sensitive, and a single email behaves exactly as before. The Setup
Wizard establishes the first (primary) root; additional roots can be added
there as a list or via the `ROOT_EMAIL` environment variable.
### Optional Variables
| Variable | Description | Default |
| - | - | - |
| | | |
| **Server Configuration** | | |
| `SERVER_PORT` | Port for the server to listen on (e.g., `8080`) | `8080` |
| `SERVER_HOST` | Host address to bind to (e.g., `0.0.0.0`) | `0.0.0.0` |
| `CORS_ALLOW_ORIGIN` | CORS allowed origins (e.g., `https://yourapp.com,https://admin.yourapp.com`) | `*` |
| `ENVIRONMENT` | Environment mode (e.g., `production`) | `production` |
| `LOG_LEVEL` | Logging level (e.g., `debug` or `warn`) | `info` |
| | | |
| **Database Configuration** | | |
| `DB_PREFIX` | Database table prefix (e.g., `notifuse`) | `notifuse` |
| `DB_NAME` | Database name (e.g., `notifuse_system`) | `${DB_PREFIX}_system` |
| `DB_SSLMODE` | SSL mode for database (e.g., `require` or `disable`) | `require` |
| `DB_MAX_CONNECTIONS` | Total max connections across all databases (e.g., `100`) | `100` |
| `DB_MAX_CONNECTIONS_PER_DB` | Max connections per workspace database (e.g., `3`) | `3` |
| `DB_CONNECTION_MAX_LIFETIME` | Maximum lifetime of a connection (e.g., `10m`) | `10m` |
| `DB_CONNECTION_MAX_IDLE_TIME` | Maximum idle time before closing connection (e.g., `5m`) | `5m` |
| | | |
| **Task Scheduler Configuration** | | |
| `TASK_SCHEDULER_ENABLED` | Enable internal task scheduler (e.g., `true` or `false`) | `true` |
| `TASK_SCHEDULER_INTERVAL` | Task execution interval in seconds (e.g., `30`) | `30` |
| `TASK_SCHEDULER_MAX_TASKS` | Maximum concurrent tasks (e.g., `10`) | `10` |
| | | |
| **Broadcast Configuration** | | |
| `BROADCAST_DATA_FEED_ALLOW_PRIVATE_HOSTS` | Allow broadcast [data feeds](/features/broadcast-campaigns) to fetch from private, loopback, and link-local addresses. This **disables SSRF protection** for data-feed requests — only enable it in trusted deployments that intentionally fetch feeds from your internal network (e.g., `true`). | `false` |
| | | |
| **Licence** | | |
| `NOTIFUSE_LICENSE_KEY` | A self-hosted [licence key](/self-hosting/licence). Wins over a key pasted in the console. Not needed to run Notifuse. | - |
| | | |
| **Privacy Settings** | | |
| `TELEMETRY` | Send anonymous usage statistics (e.g., `true` or `false`) | `true` |
| `CHECK_FOR_UPDATES` | Check for new versions (e.g., `true` or `false`) | `true` |
| | | |
| **SMTP Bridge Configuration** | | |
| `SMTP_BRIDGE_ENABLED` | Enable SMTP bridge server for transactional emails (e.g., `true` or `false`) | `false` |
| `SMTP_BRIDGE_PORT` | SMTP bridge port (e.g., `587` for STARTTLS, `465` for implicit TLS) | `587` |
| `SMTP_BRIDGE_DOMAIN` | Public domain name for SMTP bridge (e.g., `smtp.yourdomain.com`) | `localhost` |
| `SMTP_BRIDGE_TLS` | TLS posture: `starttls`, `implicit`, or `off` (see below). Auto-resolves when unset. | auto |
| `SMTP_BRIDGE_TLS_CERT_BASE64` | Base64-encoded TLS certificate (required for `starttls` and `implicit`) | - |
| `SMTP_BRIDGE_TLS_KEY_BASE64` | Base64-encoded TLS private key (required for `starttls` and `implicit`) | - |
| | | |
| **Single Sign-On (OIDC)** — see [SSO guide](/self-hosting/sso) | | |
| `OIDC_ENABLED` | Enable Single Sign-On via OpenID Connect (e.g., `true` or `false`) | `false` |
| `OIDC_ISSUER_URL` | IdP issuer URL, must be https (e.g., `https://accounts.google.com`) | - |
| `OIDC_CLIENT_ID` | OAuth2 client ID from your identity provider | - |
| `OIDC_CLIENT_SECRET` | OAuth2 client secret (encrypted at rest; never sent to the browser) | - |
| `OIDC_REDIRECT_URI` | Callback URL registered at the IdP. Derived from `API_ENDPOINT` when empty. | `/api/user.oidc.callback` |
| `OIDC_SCOPES` | Space-separated scopes (`openid` always included) | `openid email profile` |
| `OIDC_BUTTON_LABEL` | Sign-in button label | `Sign in with SSO` |
| `OIDC_AUTO_CREATE_USERS` | Auto-create accounts on first SSO login (requires `OIDC_ALLOWED_DOMAINS`) | `false` |
| `OIDC_ALLOWED_DOMAINS` | Comma/semicolon-separated email-domain allowlist gating auto-creation | - |
| | | |
| **Tracing Configuration** | | |
| `TRACING_ENABLED` | Enable tracing (e.g., `true`) | `false` |
| `TRACING_SERVICE_NAME` | Service name for tracing (e.g., `notifuse-production`) | `notifuse-api` |
| `TRACING_SAMPLING_PROBABILITY` | Sampling probability (e.g., `0.05`) | `0.1` |
| `TRACING_TRACE_EXPORTER` | Trace exporter: jaeger/zipkin/stackdriver/datadog/xray/none (e.g., `jaeger` or `datadog`) | `none` |
| `TRACING_JAEGER_ENDPOINT` | Jaeger endpoint (e.g., `http://jaeger:14268/api/traces`) | `http://localhost:14268/api/traces` |
| `TRACING_ZIPKIN_ENDPOINT` | Zipkin endpoint (e.g., `http://zipkin:9411/api/v2/spans`) | `http://localhost:9411/api/v2/spans` |
| `TRACING_STACKDRIVER_PROJECT_ID` | Stackdriver project ID (e.g., `my-gcp-project-id`) | - |
| `TRACING_AZURE_INSTRUMENTATION_KEY` | Azure Monitor instrumentation key (e.g., `12345678-1234-1234-1234-123456789012`) | - |
| `TRACING_DATADOG_AGENT_ADDRESS` | Datadog agent address (e.g., `datadog-agent:8126`) | `localhost:8126` |
| `TRACING_DATADOG_API_KEY` | Datadog API key (e.g., `1234567890abcdef1234567890abcdef`) | - |
| `TRACING_XRAY_REGION` | AWS X-Ray region (e.g., `us-east-1`) | `us-west-2` |
| `TRACING_AGENT_ENDPOINT` | General agent endpoint (e.g., `monitoring-agent:8126`) | `localhost:8126` |
| `TRACING_METRICS_EXPORTER` | Metrics exporter: stackdriver/prometheus/datadog/none (e.g., `prometheus`) | `none` |
| `TRACING_PROMETHEUS_PORT` | Prometheus metrics port (e.g., `9464`) | `9464` |
## SMTP Bridge Configuration
The SMTP Bridge feature allows you to connect SaaS applications that only provide SMTP integration (like Supabase Auth, Firebase, Auth0, etc.) to Notifuse. This gives you full control over email designs and branding using Notifuse's MJML editor, instead of being stuck with default SaaS templates.
See [SMTP Bridge usage documentation](/concepts/transactional-api#smtp-bridge) for examples of how to send emails once configured.
### TLS Modes
The bridge supports three TLS postures via `SMTP_BRIDGE_TLS`:
| Mode | Description | Typical port |
| - | - | - |
| `starttls` | Listens plaintext; clients must upgrade via `STARTTLS` before `AUTH`. Default when certs are provided. | `587` |
| `implicit` | TLS-from-byte-1 (SMTPS). Required for clients that only speak implicit TLS. | `465` |
| `off` | No TLS. Only safe when fronted by a TLS-terminating reverse proxy (HAProxy, Caddy layer4, stunnel, nginx Mail, Postfix) on a trusted network. | any |
When `SMTP_BRIDGE_TLS` is unset, Notifuse picks `starttls` if both cert and key are provided, otherwise `off` in development.
In production (`ENVIRONMENT=production`), the bridge refuses to start without TLS configured. You must either provide `SMTP_BRIDGE_TLS_CERT_BASE64` and `SMTP_BRIDGE_TLS_KEY_BASE64`, or explicitly set `SMTP_BRIDGE_TLS=off` if you're running behind a TLS-terminating reverse proxy.
### Production Setup with Let's Encrypt
For production deployments, use valid TLS certificates from Let's Encrypt using certbot with DNS challenges.
#### Step 1: Install Certbot
```bash theme={null}
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install certbot
# macOS
brew install certbot
# CentOS/RHEL
sudo yum install certbot
```
#### Step 2: Generate Certificate with DNS Challenge
DNS challenge is recommended because it doesn't require opening port 80 or 443, and works even if your SMTP server is on a different port.
```bash theme={null}
# Generate certificate using DNS challenge
sudo certbot certonly \
--manual \
--preferred-challenges dns \
--email admin@yourdomain.com \
--agree-tos \
-d smtp.yourdomain.com
```
When prompted, certbot will ask you to create a DNS TXT record:
```
Please deploy a DNS TXT record under the name
_acme-challenge.smtp.yourdomain.com with the following value:
XyZ123AbC456...
Before continuing, verify the record is deployed by running:
dig -t txt _acme-challenge.smtp.yourdomain.com
```
#### Step 3: Add DNS TXT Record
Add the TXT record to your DNS provider:
```
Type: TXT
Name: _acme-challenge.smtp
Value: XyZ123AbC456... (the value provided by certbot)
TTL: 300 (5 minutes)
```
Wait a few minutes for DNS propagation, verify with:
```bash theme={null}
dig -t txt _acme-challenge.smtp.yourdomain.com
# Or using nslookup
nslookup -type=TXT _acme-challenge.smtp.yourdomain.com
```
Press Enter in certbot to continue once the record is verified.
#### Step 4: Encode Certificates to Base64
After certbot successfully generates the certificates, encode them to base64:
```bash theme={null}
# Certificate location (typically):
# Certificate: /etc/letsencrypt/live/smtp.yourdomain.com/fullchain.pem
# Private Key: /etc/letsencrypt/live/smtp.yourdomain.com/privkey.pem
# Encode certificate to base64 (single line)
sudo cat /etc/letsencrypt/live/smtp.yourdomain.com/fullchain.pem | base64 -w 0 > cert_base64.txt
# Encode private key to base64 (single line)
sudo cat /etc/letsencrypt/live/smtp.yourdomain.com/privkey.pem | base64 -w 0 > key_base64.txt
# On macOS, omit the -w flag:
sudo cat /etc/letsencrypt/live/smtp.yourdomain.com/fullchain.pem | base64 > cert_base64.txt
sudo cat /etc/letsencrypt/live/smtp.yourdomain.com/privkey.pem | base64 > key_base64.txt
```
#### Step 5: Add to Environment Variables
Copy the base64-encoded values to your `.env` file:
```bash theme={null}
SMTP_BRIDGE_ENABLED=true
SMTP_BRIDGE_DOMAIN=smtp.yourdomain.com
SMTP_BRIDGE_PORT=587
SMTP_BRIDGE_TLS=starttls
SMTP_BRIDGE_TLS_CERT_BASE64=""
SMTP_BRIDGE_TLS_KEY_BASE64=""
```
Certbot automatically sets up a renewal cron job. After certificate renewal, you'll need to
re-encode the certificates to base64 and update your environment variables.
### Development Setup with Self-Signed Certificates
For local development, you can use self-signed certificates:
```bash theme={null}
# Using the provided script
./scripts/generate-dev-certs.sh localapi.notifuse.com
# Or manually with openssl
openssl req -x509 -newkey rsa:2048 \
-keyout dev-cert.key.pem \
-out dev-cert.cert.pem \
-days 365 -nodes \
-subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
# Encode to base64
cat dev-cert.cert.pem | base64 > cert_base64.txt
cat dev-cert.key.pem | base64 > key_base64.txt
```
**⚠️ Warning**: Self-signed certificates are for development only. Never use them in production!
## Configuration Management
**Setup Wizard vs Environment Variables**
* **Setup Wizard**: Ideal for quick deployments and testing. Configuration is stored securely in the database and can be managed through the web interface.
* **Environment Variables**: Recommended for production deployments. Provides better security for sensitive data and allows configuration management through your deployment pipeline.
* **Priority**: Environment variables always take precedence over database settings when both are present.
**For Production Deployments**: We recommend using environment variables for sensitive configuration (SMTP credentials, SECRET\_KEY) and the Setup Wizard or admin interface for non-sensitive settings (API endpoint, etc.).
## Programmatic Authentication
For CI/CD pipelines and automated deployments, a root user can authenticate programmatically using HMAC-SHA256 signature, bypassing magic link authentication. When `ROOT_EMAIL` lists multiple addresses, use any one of them as `ROOT_EMAIL` below — the signed email must exactly match one of the configured root emails.
```bash theme={null}
#!/bin/bash
# Configuration
NOTIFUSE_URL="https://your-notifuse-instance.com"
SECRET_KEY="your-secret-key"
ROOT_EMAIL="admin@example.com"
TIMESTAMP=$(date +%s)
# Generate HMAC signature
MESSAGE="${ROOT_EMAIL}:${TIMESTAMP}"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET_KEY" | awk '{print $2}')
# Authenticate and get JWT token
curl -X POST "${NOTIFUSE_URL}/api/user.rootSignin" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${ROOT_EMAIL}\",\"timestamp\":${TIMESTAMP},\"signature\":\"${SIGNATURE}\"}"
```
The endpoint returns a JWT token for subsequent API requests. Timestamps must be within 60 seconds of server time, and requests are rate limited to 5 attempts per 5 minutes.
## Third-party Hosting Platforms
Notifuse is also available on the following third-party platforms.
These platforms are not officially supported and often run outdated versions of Notifuse. For the latest version, automatic updates, and official support, use [Notifuse Cloud](https://cloud.notifuse.com).
* [PikaPods](https://www.pikapods.com/pods?run=notifuse)
* [RepoCloud](https://repocloud.io/details/Notifuse/?ref=67eysha)
* [Sealos](https://sealos.io/products/app-store/notifuse)
* [Northflank](https://northflank.com/stacks/deploy-notifuse)
# Licence keys
Source: https://docs.notifuse.com/self-hosting/licence
Self-hosting Notifuse is free. Six capabilities need a licence key. This page says which, what a key costs, how to install one, and what happens without it.
Notifuse v40 and later are published under the [Business Source License 1.1](https://github.com/notifuse/notifuse/blob/main/LICENSE), and every release becomes AGPL-3.0-or-later four years after it ships. You may run Notifuse in production, at any scale, modify it, and host it for others. Everything below is free except the six capabilities in the next section.
## What needs a key
| Capability | What is refused, and when |
| - | - |
| **More than three workspaces** | Creating a fourth workspace on one deployment. Workspaces you already have are never touched. |
| **Granular permissions** | Saving a permission set that is not full access — for a member, an invitation, or an API key. Permissions already granted stay enforced; removing a member is always allowed. |
| **Amazon SES tenant isolation** | Provisioning a new SES tenant. A tenant already provisioned keeps sending. |
| **Template translations** | Adding a language to a template, or editing one already there. Translations already saved keep being sent. |
| **Single sign-on (OIDC)** | The sign-in page stops offering the SSO button. Nobody is locked out: login-code sign-in always works, and open sessions are unaffected. |
| **Audit logs** | Recording. Without a key covering audit logs nothing is written; the [Logs → Audit logs tab](/features/audit-logs), its export and its retention setting stay available and simply show what was recorded while a key covered it. A marker entry says when recording stopped and resumed. |
Everything else — sending, contacts, broadcasts, automations, the transactional API, web analytics, every provider integration — needs no key, has no meter, and never will. The full list is at [notifuse.com/pricing/self-hosted](https://notifuse.com/pricing/self-hosted#never).
**A refusal is one action, at the moment you take it.** Nothing is deleted or hidden, and the console is never put into a restricted mode: it greys out the one control the server would refuse — switching a translation on, narrowing a member's permissions, switching SES tenant isolation on — and says which licence covers it, while everything you already have stays visible and can be removed. The send path contains no licence check of any kind: scheduled broadcasts, transactional email, webhooks and API keys keep working in every licence state, including one with no key at all. An installation that already exceeds a limit keeps everything it has.
## Tiers
| | Studio | Agency | Enterprise |
| - | - | - | - |
| Price | \$290 / year | \$890 / year | \$1,990 / year |
| Workspaces | 5 | 15 | 15 |
| Granular permissions | ✓ | ✓ | ✓ |
| SES tenant isolation | ✓ | ✓ | ✓ |
| Template translations | ✓ | ✓ | ✓ |
| Single sign-on (OIDC) | | | ✓ |
| Audit logs | | | ✓ |
One key covers one deployment — one PostgreSQL database, however many API containers serve it. Buy at [notifuse.com/pricing/self-hosted](https://notifuse.com/pricing/self-hosted); for anything else, write to [hello@notifuse.com](mailto:hello@notifuse.com).
## Getting your key
1. **Pay.** Checkout is by card, through Stripe, as an annual subscription. There is no trial: the free tier is the trial, and a 14-day refund on the first purchase replaces it.
2. **Your first key arrives by email, within a minute.** It is valid for **30 days**. The email is titled *Your Notifuse licence key* and contains the key itself.
3. **Your one-year key follows automatically.** Once the 14-day refund window has closed, a second email delivers the key that runs to the end of your subscription year. Nothing to do on your side.
At each annual renewal a fresh key is emailed. Install it the same way; the old key keeps working until its own expiry.
## Installing a key
Either of two ways. The environment variable wins.
**In the console.** Sign in as an instance administrator (a `ROOT_EMAIL`), open **Settings → Licence**, paste the key, and save. It is stored in the database and applies immediately, with no restart, on the API container that received it.
**As an environment variable.** Set `NOTIFUSE_LICENSE_KEY` to the key and restart. While it is set, the console refuses to save a key rather than store one that would lose to the variable at the next restart.
**Running more than one API container?** Use the environment variable. A key pasted in the console reaches the other containers only at their next restart — until then, requests are licensed or not depending on which container answers them. One container, either way works.
**Verification is offline.** The key is checked against a public key compiled into the binary. There is no activation step, no account, and no call to Notifuse — an installation with no outbound network access is licensed exactly like any other. If [telemetry](/self-hosting/installation#optional-variables) is enabled, it reports the licence tier and whether SSO is configured, and nothing else about the key.
## Expiry and grace
A key carries an expiry date, shown in **Settings → Licence**. After it, the key keeps granting everything it granted for a further **30 days**, then the installation falls back to the free tier — which, again, removes nothing you already have. The console shows a banner during the grace period.
## Lost your key?
The key is in the purchase email. If that is gone, ask for it again at [cloud.notifuse.com/api/licences/portal](https://cloud.notifuse.com/api/licences/portal) — enter the address the licence was bought with and the current key is re-sent to it — or write to [hello@notifuse.com](mailto:hello@notifuse.com).
## Refunds and cancellation
A licence may be refunded within 14 days of the first purchase, no questions asked, by email. Cancel at any time; the key you hold runs to its expiry. Both are governed by [section 7.5 of the Terms](https://notifuse.com/terms#pricing).
# Single Sign-On (OIDC)
Source: https://docs.notifuse.com/self-hosting/sso
Let users sign in to Notifuse with your identity provider (Google Workspace, Keycloak, Okta, …) using OpenID Connect, alongside magic-code login.
Notifuse supports **Single Sign-On (SSO)** via [OpenID Connect](https://openid.net/connect/) (OIDC), so your team can sign in with an external identity provider (IdP) such as Google Workspace, Keycloak, Okta, Microsoft Entra ID, Authentik, or Dex.
SSO runs **alongside** the built-in magic-code login — it does not replace it. When SSO is enabled, the sign-in page shows an additional **"Sign in with SSO"** button next to the email form; either method signs the user into the same account.
**SSO needs a licence that includes it** (the Enterprise tier). On an installation without one, the **Sign in with SSO** button is not offered, however SSO is configured — everyone signs in with a login code instead. Nobody is locked out: every SSO account carries a verified email address, login-code sign-in always works, and sessions already open are unaffected. The startup log says `sso_gated=true` when this applies, and **Settings → System** says so next to the switch. Installing a licence that includes SSO brings the button back immediately, with no restart. See [Licence keys](/self-hosting/licence).
SSO is **off by default**. Existing deployments that don't configure it see no change — the sign-in page stays exactly as before, with only the magic-code form.
## How it works
Notifuse acts as an OIDC *Relying Party* using the **Authorization Code flow with PKCE**:
1. The user clicks **Sign in with SSO** → Notifuse redirects them to your IdP.
2. The user authenticates at the IdP (with whatever MFA/policies you enforce there).
3. The IdP redirects back to `…/api/user.oidc.callback`; Notifuse verifies the ID token server-side (signature, issuer, audience, nonce, expiry) and signs the user in.
The IdP's tokens are exchanged and verified entirely on the server — only Notifuse's own session token ever reaches the browser. PKCE is always enabled, and the client secret is **never** exposed to the frontend.
**Login is not the same as workspace access.** A successful SSO login authenticates the *user*; it does not grant access to any workspace. SSO users still need a workspace invitation (or to be a `ROOT_EMAIL`) before they can do anything. See [Provisioning](#provisioning) below.
## Configuration
OIDC can be configured three ways, in this precedence order:
1. **Environment variables** (`OIDC_*`) — recommended for production; these always win.
2. **Setup Wizard** — an optional **SSO** step on first launch.
3. **Settings → SSO** — an admin drawer in the console after install.
When a value is set via an environment variable, the matching field is shown as read-only in the wizard/settings UI. Saving OIDC settings from the UI triggers a graceful server restart to reload the configuration (the same behavior as SMTP settings). The client secret is **encrypted at rest** in the database.
### Environment variables
| Variable | Description | Default |
| - | - | - |
| `OIDC_ENABLED` | Master switch. When `false`/unset, SSO is disabled and the button is hidden. | `false` |
| `OIDC_ISSUER_URL` | Your IdP's issuer URL (must be **https**). Notifuse discovers the rest from `/.well-known/openid-configuration`. Required when enabled. | - |
| `OIDC_CLIENT_ID` | OAuth2 client ID from your IdP. Required when enabled. | - |
| `OIDC_CLIENT_SECRET` | OAuth2 client secret (confidential client). Encrypted at rest; never sent to the browser. Required when enabled. | - |
| `OIDC_REDIRECT_URI` | The callback URL registered at your IdP. If empty, derived as `/api/user.oidc.callback`. | derived from `API_ENDPOINT` |
| `OIDC_SCOPES` | Space-separated scopes. `openid` is always included; `email` is required and `profile` supplies the optional display name. | `openid email profile` |
| `OIDC_BUTTON_LABEL` | Text shown on the sign-in button. | `Sign in with SSO` |
| `OIDC_AUTO_CREATE_USERS` | Just-in-time provisioning. When `true`, requires `OIDC_ALLOWED_DOMAINS`. See [Provisioning](#provisioning). | `false` |
| `OIDC_ALLOWED_DOMAINS` | Comma/semicolon/space-separated email-domain allowlist that gates auto-creation. **Mandatory** when `OIDC_AUTO_CREATE_USERS=true` (the server refuses to boot otherwise). | - |
When `OIDC_ENABLED=true`, the server validates the configuration at startup and **fails fast** on an invalid one (missing issuer/client/secret, a non-https issuer, or auto-create with no allowlist). The same validation runs before the Settings drawer saves, so a bad config is rejected with an error rather than persisted.
## Provisioning
By default, SSO is **invited-users-only**: a user can sign in with SSO only if they already have a Notifuse account (created via the setup root, a workspace invitation, or the API). An SSO login for an unknown email is rejected with a "no account — ask to be invited" message. This preserves Notifuse's no-self-signup model.
To let new users be created automatically on first SSO login, set:
```bash theme={null}
OIDC_AUTO_CREATE_USERS=true
OIDC_ALLOWED_DOMAINS=yourcompany.com
```
With auto-create on, an account is created on first login **only** when the IdP asserts a verified email **and** its domain is in `OIDC_ALLOWED_DOMAINS`. Even then, the new user has no workspace access until invited.
Users are matched by the durable `(issuer, subject)` pair from the IdP, not by email — so a user keeps the same account even if their email changes at the IdP. Email is used only to link an SSO identity to an existing invited account on first login. A `ROOT_EMAIL` account can never be auto-linked to an external identity (a safeguard against platform-admin takeover).
## Identity provider setup
The callback/redirect URL to register at every provider is:
```
https:///api/user.oidc.callback
```
The redirect URI must match **byte-for-byte** between your IdP and Notifuse. If you run behind a reverse proxy that rewrites the scheme or host, set `OIDC_REDIRECT_URI` explicitly to the externally-visible URL — otherwise the IdP returns a `redirect_uri_mismatch` error.
### Google Workspace
1. In the [Google Cloud Console](https://console.cloud.google.com/apis/credentials), go to **APIs & Services → Credentials → Create credentials → OAuth client ID**.
2. Choose **Web application**.
3. Under **Authorized redirect URIs**, add `https:///api/user.oidc.callback`.
4. Copy the **Client ID** and **Client secret**.
5. Configure Notifuse:
```bash theme={null}
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://accounts.google.com
OIDC_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
OIDC_CLIENT_SECRET=your-client-secret
# Restrict to your organization's domain:
OIDC_ALLOWED_DOMAINS=yourcompany.com
```
Personal `@gmail.com` accounts and your Workspace accounts share the `accounts.google.com` issuer. Use `OIDC_ALLOWED_DOMAINS` (or keep the default invited-only mode) to restrict sign-in to your organization.
### Keycloak
1. In your realm, create a client: **Client type** OpenID Connect, **Client authentication** ON (confidential), **Standard flow** enabled.
2. Set **Valid redirect URIs** to `https:///api/user.oidc.callback`.
3. Copy the secret from the client's **Credentials** tab.
4. Configure Notifuse:
```bash theme={null}
OIDC_ENABLED=true
OIDC_ISSUER_URL=https://keycloak.example.com/realms/
OIDC_CLIENT_ID=notifuse
OIDC_CLIENT_SECRET=your-client-secret
```
The default Keycloak client scopes already emit `email`, `email_verified`, `name`, and `sub`. If someone has stripped the email mapper from your client, SSO logins will be rejected (no email claim) — re-add the built-in `email` scope.
### Other providers
Any OpenID-Connect–certified provider works the same way — set `OIDC_ISSUER_URL` to the provider's issuer, register the redirect URI, and supply the client ID/secret. This includes Okta, Microsoft Entra ID (Azure AD), Authentik, and Dex.
## Security notes
* Only two non-secret values reach the browser: `window.OIDC_ENABLED` and the button label. The client ID, client secret, and issuer URL are never exposed in the page.
* ID tokens are verified with the issuer's published keys (auto-rotated via JWKS); signature, issuer, audience, nonce, and expiry are all checked, and only asymmetric algorithms (RS256/ES256) are accepted.
* The flow's CSRF/nonce/PKCE state is held in a short-lived, encrypted, `HttpOnly` cookie.
* OIDC HTTP calls (discovery, key fetch, token exchange) are time-bounded, so a slow or unreachable issuer cannot stall the sign-in path; magic-code login is unaffected.
## Troubleshooting
| Symptom | Cause & fix |
| - | - |
| **"Single sign-on is temporarily unavailable"** after clicking SSO | Notifuse can't reach or discover the issuer. Verify `OIDC_ISSUER_URL` is correct, reachable from the server, and serves a discovery document. Notifuse retries automatically once the issuer is back. |
| **"No Notifuse account is linked… ask to be invited"** | Invited-only mode (the default) and this email has no account. Invite the user to a workspace, or enable `OIDC_AUTO_CREATE_USERS` with an allowlist. |
| **`redirect_uri_mismatch` / `invalid_redirect_uri` at the IdP** | The redirect URI registered at the IdP doesn't exactly match what Notifuse sends. Register `…/api/user.oidc.callback`, and set `OIDC_REDIRECT_URI` if you're behind a proxy that rewrites scheme/host. |
| **The server won't start after enabling OIDC** | Configuration validation failed. Check that `OIDC_ISSUER_URL` is https and `OIDC_CLIENT_ID`/`OIDC_CLIENT_SECRET` are set, and that `OIDC_ALLOWED_DOMAINS` is non-empty when `OIDC_AUTO_CREATE_USERS=true`. |
| **A user can sign in but can't access any workspace** | Expected — login is not authorization. Invite the user to the relevant workspace (or add them to `ROOT_EMAIL`). |
Running multiple Notifuse replicas behind a load balancer? The one-time code used to complete the SSO handoff is held per-instance, so enable **sticky sessions** for SSO to work reliably across replicas. Single-instance deployments need no extra configuration.
# Newsletter Form Input
Source: https://docs.notifuse.com/tutorials/newsletter-form-input
This tutorial will guide you through creating a newsletter subscription form and integrating it with Notifuse to collect and manage subscriber contacts
Newsletter forms are essential for growing your email list and engaging with your audience. With Notifuse, you can easily collect subscriber information and automatically add them to your contact lists for future campaigns.
## Prerequisites
Before you begin, make sure you have:
* A Notifuse workspace set up
* Basic understanding of HTML/JavaScript
* Access to your website or application where you want to embed the form
## Step 1: Create a Contact List
First, create a dedicated list for your newsletter subscribers:
1. Navigate to **Lists** in your Notifuse dashboard
2. Click **Create New List**
3. Name your list (e.g., "Newsletter Subscribers")
4. Add a description and save
## Step 2: Generate a Complete Working Example
Instead of manually coding the form, you can use AI to generate a complete, working example:
**[📝 Generate with ChatGPT](https://chatgpt.com/)**
**[📝 Generate with Claude.ai](https://claude.ai/)**
Copy and paste this prompt to either AI assistant:
```
Create a newsletter subscription box using HTML, CSS, and JavaScript. The form should:
1. Collect email address (required) and first name (optional)
2. Have modern, responsive styling with a clean minimalist design
3. Include client-side email validation
4. Show loading states during submission
5. Display success/error messages
6. Use the Notifuse /subscribe API endpoint
Read the API reference: https://docs.notifuse.com/api-reference/subscribe-to-email-lists
The form should POST to: https://YOUR_NOTIFUSE_DOMAIN/subscribe
With this JSON structure:
{
"workspace_id": "YOUR_WORKSPACE_ID",
"contact": {
"email": "user@example.com",
"first_name": "John"
},
"list_ids": ["YOUR_LIST_ID"]
}
Make it production-ready with proper error handling, user feedback, and mobile-friendly design. Use placeholder values that users can easily replace.
```
## Step 3: API Integration (Manual Implementation)
**Note**: This step is only for those who didn't use the AI prompt above. If you used the AI prompt, your form should already have the correct API integration.
For manual implementation, use the Notifuse `/subscribe` endpoint to handle newsletter subscriptions. This is a public endpoint that doesn't require authentication:
```javascript theme={null}
async function subscribeToNewsletter(email, firstName, workspaceId, listIds) {
try {
const response = await fetch('https://YOUR_NOTIFUSE_DOMAIN/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
workspace_id: workspaceId,
contact: {
email: email,
first_name: firstName || null
},
list_ids: listIds
})
})
if (response.ok) {
const result = await response.json()
return { success: true, data: result }
} else {
const error = await response.json()
return { success: false, error: error.error }
}
} catch (error) {
return { success: false, error: 'Network error occurred' }
}
}
// Usage example
document.getElementById('newsletter-form').addEventListener('submit', async function (e) {
e.preventDefault()
const email = document.getElementById('email').value
const firstName = document.getElementById('firstName').value
const result = await subscribeToNewsletter(email, firstName, 'YOUR_WORKSPACE_ID', [
'YOUR_LIST_ID'
])
if (result.success) {
// Show success message
alert('Thank you for subscribing!')
} else {
// Show error message
alert(`Error: ${result.error}`)
}
})
```
Replace the placeholder values in your code:
* `YOUR_NOTIFUSE_DOMAIN`: Your unique Notifuse domain (e.g., `demo.notifuse.com`)
* `YOUR_WORKSPACE_ID`: Your workspace ID
* `YOUR_LIST_ID`: The ID of your newsletter list
## Step 5: Testing Your Form
1. Test the form with a valid email address
2. Check your Notifuse dashboard to confirm the contact was added to your list
3. If the list has double opt-in enabled, check that confirmation emails are sent
## Best Practices
* **Double Opt-in**: Enable double opt-in on your lists for better engagement and compliance
* **Privacy Compliance**: Add links to your privacy policy and terms of service
* **Error Handling**: Provide clear error messages for different scenarios
* **Mobile Responsive**: Ensure your form works well on all devices
* **Public Lists**: Make sure your list is marked as "public" to allow subscriptions via the `/subscribe` endpoint
## Troubleshooting
### Common Issues
**"list is not public" Error**: Ensure your list is configured as public in your Notifuse dashboard.
**"invalid contact: invalid email format" Error**: Check that email validation is working correctly on the frontend.
**Network Errors**: Verify that `YOUR_NOTIFUSE_DOMAIN` is correct and accessible.
**CORS Errors**: The `/subscribe` endpoint supports CORS, but make sure you're using the correct domain.
## Next Steps
Once your newsletter form is working:
1. Create welcome email templates for new subscribers
2. Set up automated welcome campaigns
3. Design regular newsletter broadcasts
4. Monitor subscription analytics in your Notifuse dashboard
## Related Documentation
* [Contacts](/concepts/contacts)
* [Lists](/concepts/lists)
* [Broadcast Campaigns](/concepts/broadcast-campaigns)
* [Transactional API](/concepts/transactional-api)
# Analytics Assistant
Source: https://docs.notifuse.com/web-analytics/ai-assistant
Ask questions about your traffic, get a written summary of a period, and let the assistant drive the dashboard.
The Web Analytics section has an AI assistant. It answers questions about your own traffic, writes a summary of the period you are looking at, and can change what is on screen for you.
It reads your reports through the same queries the dashboard itself runs, so an answer it gives and the chart beside it cannot disagree.
This is the Web Analytics surface of the workspace [AI Assistant](/features/ai-assistant), which runs on your own provider key. Until an LLM integration is configured, the panel offers a link to set one up instead of a chat box.
## Opening it
Once Web Analytics is enabled, a button sits in the bottom-right corner of the Dashboard, Explore and Goals sections. It is not available on the Filters section, which configures attribution rules rather than reading a report.
An empty conversation offers a few starters — summarise the period, explain a change, top sources, best and worst pages. Clicking one sends it.
## What it can answer
Anything the reports can express:
| Question | What it does |
| - | - |
| "How many sessions last week, and how does that compare?" | Runs the totals for both windows and reports the change |
| "Which campaigns drove the most conversions this month?" | Groups goals by UTM campaign |
| "What are my worst landing pages by bounce rate?" | Breaks sessions down by landing page, sorted the way you asked |
| "Show me traffic by hour of day" | A bucketed series over the period |
| "Summarise this period" | Pulls headline metrics, the trend and the leading breakdowns in one pass, then writes what actually moved |
Ask it something the data cannot answer and it will say so rather than guess. It is instructed not to claim a cause it cannot see in the numbers — a drop that lines up with one campaign ending is reported as exactly that, not as a conclusion.
## Driving the dashboard
The assistant can change the screen as well as read it:
* set the period, a custom date range, the comparison window or the timezone
* apply, add to, or clear the dashboard filters
* open an Explore report grouped by the dimensions you asked for
* move between the Dashboard, Explore and Goals sections
"Show me last 30 days for organic search only" changes the period and the filter, and the whole dashboard follows. Changes it makes are ordinary dashboard state — undo them the same way you would your own.
## What it will not do
| Boundary | Why |
| - | - |
| It never groups, sorts or filters by visitor email address | Asking for a report broken down by contact would send a list of identified visitors' addresses to your AI provider. The aggregate question still works: the **contacts** measure counts distinct identified visitors. |
| It does not report per-session coordinates | Latitude and longitude identify individuals. Country, region and city stay available. |
| It cannot create goals, saved filters or segments | It reports and navigates; it does not change your configuration. |
| It does not invent numbers | Every figure it states comes from a query it ran in that conversation. |
If the dashboard is already filtered to a single person when you ask a question, the assistant is told a filter is in force — otherwise it would misread every number — but not what the filter's value is.
## Data freshness
Sessions are written in batches, so the last minute or so of activity may not be in an answer yet, and a period that includes today includes an incomplete day. The assistant says so when it matters. For what is happening right now, use the live view instead.
## Model choice and cost
Answering here depends on tool calling: the assistant picks a query, reads the rows that come back, and reasons over them. If answers are vague, or it repeatedly says it cannot find a dimension, try a stronger model before assuming the data is at fault.
A data question also costs more than a plain chat of the same length. It takes at least two requests to the provider — one to choose the query, one to read the result — and a period summary is a large single result to read.
Provider options, model selection and how the panel reports cost are on the [AI Assistant](/features/ai-assistant) page.
## Related
* [AI Assistant](/features/ai-assistant) — setting it up, providers, and the email and blog surfaces
* [Reports](/web-analytics/reports) — the dimensions and measures the assistant queries
* [Goals](/web-analytics/goals) — how conversions get their type and value
* [Identifying visitors](/web-analytics/identifying-visitors) — what makes the `contacts` measure non-zero
# Annotations
Source: https://docs.notifuse.com/web-analytics/annotations
Mark the moments that explain a chart — a launch, a campaign, an outage.
An annotation is a date you put on the record: the day you shipped, the morning the site went down, the hour a campaign went out. It appears as a labelled vertical line on your Web Analytics charts, so a spike or a dip has its explanation attached to it rather than living in someone's memory.
Annotations belong to the workspace and are managed under **Web Analytics** → **Annotations**.
## Adding an Annotation
Click **Add** and fill in the form:
| Field | Required | Notes |
| - | - | - |
| Date | Yes | The day being marked. |
| Time | Yes | Defaults to 12:00, which is usually precise enough for a daily chart. |
| Timezone | Yes | Starts on the timezone your charts are drawn in. |
| Title | Yes | Up to 100 characters. This is what prints on the chart. |
| Description | No | Up to 500 characters, shown in the chart tooltip and in the list. |
| Color | No | Presets — green, red, amber, blue and the purple broadcasts use. Blue if you pick nothing. |
The timezone is stored with the annotation so the moment redisplays exactly as it was typed. An outage entered as 9am in Tokyo keeps reading as 9am for a colleague in Paris, instead of being rewritten into their own morning.
## Where They Appear
On the **Dashboard** chart, and on the chart inside a goal's drawer on the **Goals** section.
Each annotation is drawn as a dotted vertical line in its own color, with its title printed at the top of the plot area — truncated there if it is long. Hovering that point on the chart adds the annotation's title and description to the tooltip, under the metric values.
The line lands on the bucket that contains the moment, so how precisely it sits depends on the granularity you are looking at. Annotations outside the period on screen are simply not drawn.
Several annotations falling in the same bucket collapse to a single line, labelled with the first title and a `+N`. Hover it and the tooltip lists all of them in full.
## Broadcast Annotations
Broadcasts annotate themselves. When a broadcast starts sending, an annotation carrying its name is written automatically, tagged **Broadcast** in the list and drawn in purple on the charts. That is what lets you see a traffic bump and the campaign that caused it on the same chart.
* It is written **only once the send genuinely begins** — after the workspace, provider and template checks have all passed. A broadcast that fails to load its templates mails nobody and is never annotated.
* One annotation per broadcast, however many times its sending task resumes.
* They are **always shown** on the charts. There is deliberately no setting to hide them.
* The annotation is independent of the broadcast afterwards: deleting the broadcast leaves it standing, because the send did happen, and deleting the annotation does nothing to the broadcast.
The **All / Manual / Broadcast** filter above the list narrows what you are reading on that page. It does not change what the charts draw.
## Editing and Deleting
Every annotation can be edited or deleted from the list, broadcast ones included — you may well want to reword an automatic title into something you will recognise in six months.
Editing changes the moment and its timezone, the text and the color only. A broadcast annotation keeps its origin and its tag however you rewrite it, and an annotation you typed yourself can never become a broadcast one.
Deleting is immediate and cannot be undone.
## Managing Them Over the API
Annotations have five endpoints, documented in the [API Reference](/api-reference):
| Endpoint | Purpose |
| - | - |
| `GET /api/annotations.list` | List, optionally filtered by `start`, `end` and `sources` |
| `GET /api/annotations.get` | Fetch one by id |
| `POST /api/annotations.create` | Create |
| `POST /api/annotations.update` | Edit |
| `POST /api/annotations.delete` | Delete |
Access follows the existing Web Analytics permission: read for `list` and `get`, write for the rest.
This is the natural place to hook your deploy pipeline or your status page, so releases and incidents land on the chart without anyone typing them.
The API creates **manual** annotations only — a caller cannot mint a broadcast one — and there is no idempotency key. A job that retries a create writes a second annotation. Deduplicate on your side, or clean up from the Annotations page.
## Notes
* `list` returns the 100 most recent annotations by default, and at most 1,000. The Annotations page and the charts both ask for the maximum.
* The Annotations page works even before the tracker is installed, unlike the rest of Web Analytics — a workspace that only sends broadcasts still collects broadcast annotations.
* [API Reference](/api-reference)
# Attribution
Source: https://docs.notifuse.com/web-analytics/attribution
Classify traffic into channels with rules you control, and rewrite history when you change them.
Every visit arrives with raw signals: a referrer, some UTM parameters, maybe an ad click ID. Attribution rules turn those into a **Channel** and a **Channel Group** you can report on.
Your own site is never one of those signals. A referrer on the same host as the page it led to is discarded and the visit is recorded as **direct** — a session ends after 30 minutes of inactivity, so a visitor who comes back to a tab, or clicks on to another page after a long read, starts a new session whose referrer is one of your own pages. Another host is a genuine referral and is kept: `docs.example.com` linking to `www.example.com` appears in your reports.
## Default Rules
A new workspace starts with 40 rules covering paid search, paid social, paid video, display, organic search, organic social, email and direct. They are ordinary rules: you can edit or delete any of them.
| Priority | Family |
| - | - |
| 900–831 | Paid, identified by ad click ID |
| 830–770 | Paid, identified by a known UTM source together with a paid UTM medium |
| 760 | Paid display, identified by referrer |
| 750 | Direct, declared by `utm_source=direct` and `utm_medium=none` |
| 745 | Email |
| 740 | Direct traffic — no referrer *and* no campaign parameters |
| 705–650 | Organic search |
| 600–510 | Organic social |
| 10 | Fallback — `not-mapped` |
Email sits above Direct Traffic deliberately. Many mail clients strip the referrer, so a click from an email otherwise looks like someone typing your address — which is why your broadcasts should carry UTM parameters.
There is no seeded rule for **referral** traffic. A plain link click from another site matches nothing and lands in `not-mapped`, by design: it surfaces in the Filters tab so you can classify those referrers once, rather than being quietly bucketed. Add your own rule on Referrer Domain when you want a referral channel.
For the same reason, the paid UTM rules require a *known* source as well as a paid medium — `utm_medium=cpc` from a network Notifuse does not ship a rule for falls through to `not-mapped` rather than being called paid.
## How Rules Are Applied
Rules run by priority, highest first, and **every matching rule contributes** — evaluation does not stop at the first match.
Precedence is per target rather than per rule:
* For each target, the highest-priority rule that writes it wins.
* A lower-priority rule can still write a target no higher-priority rule claimed. A rule that only sets `custom_3` therefore applies even when a higher-priority rule already set the channel.
* Two matching rules at the *same* priority both run, and the later one overwrites — a tie goes to the last rule, not the first.
* `set as default` never claims a target, so a lower-priority `set` still overrides a default written earlier. That is how the priority-10 fallback works.
## Writing a Rule
A rule is a set of conditions and the values to write when they all match. All conditions of a rule must match for it to apply.
* **Conditions** test any tracked signal: the referrer, referrer domain and referrer path, is-direct, the landing page, domain and path, the current path, every UTM parameter and the ad click ID, plus device, browser, browser type, OS, user agent, connection type, language and timezone.
* **Operators** are `equals`, `not equals`, `contains`, `not contains`, `is empty`, `is not empty` and `regex`.
* **Operations** then `set` a value, `set` it only as a *default* where nothing else has written one, or `unset` it.
* **Targets** are Channel, Channel Group, any of the five UTM fields, Referrer Domain, Is Direct, and `custom_1` through `custom_10`.
Writing a custom dimension from a rule is how you classify traffic you could not tag at the source — a partner's referrer becomes a partner name, say, without asking them to add parameters.
## Ad Click IDs
The tracker recognises thirteen ad click ids on the landing URL: `gclid`, `wbraid`
and `gbraid` (Google Ads), `dclid` (Google Display), `fbclid` (Meta), `msclkid`
(Microsoft), `ttclid` (TikTok), `li_fat_id` (LinkedIn), `twclid` (X), `epik`
(Pinterest), `ScCid` (Snapchat), `rdt_cid` (Reddit) and `qclid` (Quora).
The first one present lands in `utm_id`, and the parameter it came from in
`utm_id_from` — so a paid click is attributable even when the campaign forgot its
UTMs. "First" means first in that list, not first in the URL, so a link carrying
both `gclid` and `fbclid` is attributed to Google.
Matching ignores case: `?sccid=`, `?ScCid=` and `?SCCID=` are all recognised, and
`utm_id_from` always records the canonical spelling above — which is what the
built-in rules match on.
To capture a click id of your own **in addition** to those thirteen, set
`extraAdClickIds: ['myclid']` in `NotifuseAnalyticsConfig`. `adClickIds` also
exists but **replaces** the list outright, and `adClickIds: []` captures none —
use it only when that is what you want. A rule matching on `utm_id_from` only
ever fires for a parameter the tracker was told to capture.
## Changing Rules Later
Rules are applied when a visit is recorded, so editing them changes what happens next, not what already happened. To apply them to existing traffic, run a **backfill** from the Filters tab.
A backfill rewrites the attribution columns on historical **session and goal** rows — which is what updates "revenue by channel" for past traffic. It never adds, removes or re-times traffic, and pageview rows are not touched at all.
A backfill **clears** Channel and Channel Group on any row its rules do not write.
Those two exist only because a rule said so, so "no rule matches" means empty.
Custom dimensions are left alone, because your tracker can set them without any
rule being involved. A rule that targets one still rewrites the rows it matches,
and an `unset_value` rule still clears them — but a row no rule matches keeps
whatever your site sent. One consequence worth knowing: a custom value written by
a rule survives that rule's deletion, since nothing can tell it from a
tracker-supplied one afterwards. Clear it with a temporary `unset_value` rule.
One backfill runs at a time, per workspace. Cancelling stops it where it is; the partitions already rewritten keep their new values, so a cancelled run leaves your data in a mixed state until you run it again.
## Where the Traffic Comes From
Broadcasts add UTM parameters to their links — see [Broadcast Campaigns → UTM Tracking](/features/broadcast-campaigns#utm-tracking). Those are exactly what the seeded Email rule looks for, so campaign traffic classifies itself with no extra work.
## Notes
* Attribution is resolved per session, from the landing page. A visitor who arrives from a search and later clicks one of your emails starts a new session with its own attribution once the first has ended.
* Goals start from their session's attribution, then re-run the rules with the goal's *own* page path. A goal matches its session unless one of your rules tests the current path.
* The rule set is versioned by a hash the server recomputes on every save. The console compares it against the version of the last completed backfill to warn you when your history was attributed under a different rule set. A backfill rewrites every historical row, not just the stale ones.
* [API Reference](/api-reference)
# Goals
Source: https://docs.notifuse.com/web-analytics/goals
Record conversions from your website and use them in segments and automations.
A goal is something worth counting: a signup, a purchase, a demo request. Your site reports it, and Web Analytics records it against the visit that produced it.
## Recording a Goal
```javascript theme={null}
NotifuseAnalytics.trackGoal({ action: 'purchase', type: 'purchase', value: 49.9 })
```
| Field | Required | Notes |
| - | - | - |
| `action` | Yes | The goal name, up to 100 bytes — 100 ASCII characters, fewer for accented or non-Latin text. |
| `type` | Yes | What kind of conversion this is: `purchase`, `subscription`, `lead`, `signup`, `booking`, `trial` or `other`. |
| `value` | No | A non-negative number — revenue, or whatever you are totalling. |
| `properties` | No | Up to 50 string values of your own, 1024 bytes each and 8 KB in total. |
`type` is required because only your site knows whether a conversion is a sale or
a lead, and the goal-based segment conditions filter on it. `trackGoal` throws
without it. Use `other` when none of the rest fit.
The current page path and its position in the visit are attached for you, so a goal always knows where it happened.
Goals are sent immediately rather than waiting for the next heartbeat. A conversion on a page the visitor is about to leave still arrives.
A negative `value` makes the whole goal invalid, and the goal is discarded — name and all, not just the number. This differs from the [Custom Events](/features/custom-events) API, which deliberately accepts negative values for refunds and chargebacks.
You do not declare goals anywhere first. The Goals tab lists whatever names your site has actually sent, ordered by conversions, showing up to the 30 most-converted goals for the period and filters you have applied.
## What a Goal Produces
Clicking a goal opens its own dashboard — the conversions and value over time, annotated like the main chart, with the sources and campaigns that produced them.
In the reports, goals give you the Goals, Total Goal Value, Avg Goal Value, Median Goal Value and Converting Sessions measures, and the Goal Name, Goal Path and Goal Value dimensions. At the session level, Sessions with Goals and Goal Value let you compare channels by what they actually converted rather than by traffic.
For an **identified** visitor, a goal also becomes a custom event on their contact timeline — which is what makes it reachable from segments and automations. See [Identifying Visitors](/web-analytics/identifying-visitors).
## Goal Names on the Contact Timeline
A goal that reaches a contact timeline is normalised first: lowercased, with anything outside ASCII `a–z`, `0–9`, `.` and `/` collapsed to a single `_`, leading and trailing `_` trimmed, and the result cut at 100 characters. So `Trial Started` arrives as `trial_started`, and it is that name an automation or segment matches on.
The kept set is ASCII-only, so accented and non-Latin letters are dropped rather than transliterated: `Café` arrives as `caf`. A name with no ASCII letters or digits at all — `購入`, say — normalises to nothing and never reaches the timeline. The goal is still recorded and still appears in the reports; only the timeline copy is skipped. Use ASCII goal names if you intend to segment or automate on them.
This is also narrower than the naming rule for events sent through the [Custom Events](/features/custom-events) API, which allows hyphens: `black-friday` sent from a web goal becomes `black_friday`.
## Using Web Goals in Segments
A web goal is as expressive as a custom event created through the API, because it
carries the same three things: a name, a value and a type.
* The **Custom Events Goal** condition matches web goals, under a specific type or
under **All types**, and counts, sums and averages their value.
* An **automation** can trigger on a custom event with the goal's normalised name.
The type is asserted by your site, not verified by Notifuse — the same page also
chooses the goal's name and value. Read revenue reporting with that in mind, as
you would for events sent through the API.
## Limits
* Up to 100 goals of one session are bridged to the contact timeline per write batch. Goals beyond that within the same batch are dropped from the timeline permanently, and the counter restarts on each batch — so a long visit that keeps converting can end up with more than 100 timeline entries in total. Either way the visit itself keeps recording; the cap only affects the timeline copy.
* A goal older than 24 hours is not bridged to a timeline. A visitor whose browser was offline for a day comes back with their visit intact, without firing "you just did this" automations a day late.
* A goal value above 1e12, a name over 100 bytes, or properties over 8 KB are dropped — the goal alone, never the rest of the visit.
## Notes
* Goals from anonymous visitors are recorded and reported normally. Only the timeline copy needs an identity.
* Bridged events carry the visit's context as properties — session id, path, landing path, UTM source / medium / campaign, device and country — alongside your own. Those keys are set by Notifuse and overwrite properties of the same name.
* A currency, an order id, or anything else the conversion carries goes in `properties` — those are sent and stored. There is no dedicated `currency` field: `goal_value` is a plain number, so if you record more than one currency, group your reports by the currency property rather than summing values across them.
* [API Reference](/api-reference)
# Identifying Visitors
Source: https://docs.notifuse.com/web-analytics/identifying-visitors
Tie a website visit to a contact, and use browsing behaviour in segments.
By default Web Analytics is anonymous: it counts visits without knowing who made them. Telling it who someone is connects their browsing to their contact record — their visits appear on the contact timeline, and their behaviour becomes available to segments.
Because `/track` is a public endpoint, an address alone proves nothing. Identification is always signed.
## identify()
Call `identify` once you know who the visitor is, with the address and a signature computed **on your server**.
```javascript theme={null}
NotifuseAnalytics.identify('alice@example.com', '')
```
Both arguments are required: calling `identify` without them rejects rather than failing quietly, as does an address over 255 characters or a signature over 64 bytes. A *wrongly signed* address is the silent case — the server discards the identity and still records the visit, anonymously.
### Computing the signature
The signature is an HMAC-SHA256, hex-encoded, over the string `wa_identify:` followed by the email address, keyed with your workspace secret key.
```javascript Node.js theme={null}
import { createHmac } from 'crypto'
function webIdentifyHmac(email, workspaceSecretKey) {
return createHmac('sha256', workspaceSecretKey)
.update('wa_identify:' + email)
.digest('hex')
}
```
```go Go theme={null}
func webIdentifyHMAC(email, workspaceSecretKey string) string {
mac := hmac.New(sha256.New, []byte(workspaceSecretKey))
mac.Write([]byte("wa_identify:" + email))
return hex.EncodeToString(mac.Sum(nil))
}
```
Sign the address exactly as you pass it to `identify` — the signature is checked before the address is normalised, so `Alice@Example.com` must be signed as `Alice@Example.com`.
Never compute this in the browser. The signature exists because anyone can post to the tracking endpoint; a secret key shipped to the page defeats it entirely.
The `wa_identify:` prefix is deliberate. It keeps this signature from being interchangeable with the one in your unsubscribe links — so a signature lifted from a forwarded email cannot identify a visitor, and one lifted from your page cannot change someone's subscriptions.
`clearIdentity()` stops later beats from carrying the identity. It does not anonymise what was already recorded.
## Identifying from Email Links
Notifuse can add a signed identity to the links of tracked emails, so a recipient who clicks one is recognised on landing without any code on your site.
This is off by default. Turn on **Identify recipients who click a tracked email link** under **Settings** → **Web Analytics**.
Unlike `identify()`, which your own server calls with your own secret, this credential is minted by Notifuse for every recipient of every tracked send — which is why it is a separate decision rather than something that follows from enabling web analytics.
The identity is an encrypted `nf_id` parameter that expires 7 days after it is minted. It is added only to links whose host is in your allowed domains, so third-party links in the same email never carry it. The tracker reads it on landing and strips it from the URL.
A send never carries one when the notification has tracking explicitly disabled, or — on the transactional API — when the message has any CC or BCC recipient, since the identity would otherwise be handed to someone who is not the contact.
Turning off **click tracking** does not remove the identity. With no redirect to wrap, the parameter is written straight onto your own link. Tracking must be disabled for the notification, or email-link identification turned off, to suppress it.
Turning the setting off also only stops *new* credentials being minted. An `nf_id` already sitting in a visitor's browser keeps identifying them until it expires, up to 7 days later.
## What Identification Records
Once a visitor is identified, that visit and everything already recorded in it is attributed to the contact:
* **On the analytics rows**: the contact's address, so Contact Email becomes a dimension and Identified Contacts becomes a measure.
* **On the contact timeline**: one entry per page they viewed and one summarising the visit, plus any goals as custom events.
If the address is not a contact yet, one is created — carrying the language and timezone the visit reported, plus the country resolved from the visitor's IP when geolocation is on. A value Notifuse cannot store, such as an unsupported language or an unrecognised timezone, is simply left off. An existing contact is never modified by a visit, and a contact created this way joins no list.
## Segments
Web behaviour is available as Activity conditions on a segment:
| Condition | Filter on |
| - | - |
| **View web page** | Path, time on page, scroll depth, page number in visit, entry type |
| **Visit website** | Entry page, exit page, pages viewed, visit duration, scroll depth, goals reached, goal value, referrer domain, UTM source / medium / campaign, channel, channel group, device, browser, operating system, country |
So "viewed `/pricing` at least twice in the last 30 days" or "visited from a newsletter campaign and reached at least one goal" are ordinary segment conditions. See [Contacts](/features/contacts) for how conditions combine.
Navigation never triggers an automation. Pageviews and visits are recorded and segmentable, but only **goals** reach automations — through the `custom_event` trigger, under the goal's normalised name. Goals carry a declared type, so they are also reachable through the Custom Events Goal condition; see [Goals](/web-analytics/goals#using-web-goals-in-segments).
## Privacy
* No cookie is set, and no identifier follows a visitor between sites.
* IP addresses are used to resolve a location and are never stored.
* An anonymous visit stays anonymous. Nothing links it retroactively to a contact identified in a later session.
* Deleting a contact removes their address everywhere it was stored: the analytics rows, their timeline entries, and the custom events created from their web goals. The traffic survives as anonymous, so historical totals do not change.
A visitor whose browser still holds a credential is re-identified — and re-created as a contact — by their next visit, even after you delete them. To stop it for good, stop calling `identify()` for that address, and turn off email-link identification if the credential came from a link. An `nf_id` minted before you turned the setting off keeps working until it expires.
## Notes
* Identification is rate limited per address and per source IP. Creating a *new* contact from an identification is additionally rate limited per workspace.
* Exceeding a limit costs the identity on that beat, never the visit.
* Each projection pass writes at most 100 pageviews per visit, keeping the most recent. A long visit projected over several passes can leave more than 100 pageview entries on the timeline.
* Goals older than 24 hours are not added to a timeline, so a replayed offline visit does not fire automations a day late.
* [API Reference](/api-reference)
# Reports
Source: https://docs.notifuse.com/web-analytics/reports
Read the dashboard, explore any dimension, and watch traffic arrive live.
**Web Analytics** is a collapsible group in the sidebar with six entries: Dashboard, Live, Explore, Goals, Filters and Annotations. Everything except Live is also a tab on the Web Analytics page, so you can move between them without leaving it.
## What Gets Recorded
| Row | Written when |
| - | - |
| **Session** | A visitor arrives. One row per visit, updated as they browse. |
| **Pageview** | Each page they open, with its engaged time and how far they scrolled. |
| **Goal** | Your site calls `trackGoal`. See [Goals](/web-analytics/goals). |
The tracker sends a beat every 7 to 30 seconds while someone is active — more often just after a page opens, tapering off — and each beat carries the whole visit so far. That means a report is never half-written: a session row always reflects everything known about that visit at the time it was last seen.
Beats stop after about ten minutes of accumulated active time on the same page. A tab left open for an hour is not counted as an hour of engagement; opening another page starts the cadence again.
## Dashboard
The headline numbers over the period you pick, with the breakdowns most people want first — sources, pages, countries, devices.
Clicking a row in any panel adds that value as a filter on the whole page, and clicking it again removes it. That is how the dashboard drills down: filters stack, so you can narrow to one country and one channel and watch every panel follow.
## Live
Everyone active in the last 30 minutes, refreshed every 10 seconds, with a map of where they are. The map plots up to 500 distinct locations, each marker carrying the number of sessions there.
## Explore
Break sessions down by any dimension, filter them, and export the result as CSV.
Explore reports on **sessions**, with four measures shown for every row:
| Measure | Meaning |
| - | - |
| Sessions | Number of visits |
| Median TimeScore | Median engaged time across those visits |
| Bounce Rate | Share of visits below your bounce threshold |
| Median Scroll Depth | How far down visitors got |
**Dimensions** are grouped as they appear in the picker:
* **Channel**: Channel, Channel Group
* **UTM**: Source, Medium, Campaign, Term, Content
* **Traffic**: Referrer, Referrer Domain, Referrer Path, Is Direct
* **Pages**: Landing Page, Landing Domain, Landing Path, Exit Path
* **Device**: Device, Browser, Browser Type, Operating System, Connection Type, screen and viewport size
* **Geo**: Country, Region, City, Language, Timezone
* **Time**: Hour of Day, Day of Week, Is Weekend, Year, Month, Day, Week Number
* **Session**: Pageview Count, Duration (ms), SDK Version
* **User**: Contact Email, once a visitor is identified
* **Custom**: `custom_1` through `custom_10`, under whatever names you gave them
Time dimensions here are cyclic, and are extracted in the report's timezone rather than UTC. "Traffic by hour of day" means the visitor's morning, not a number that shifts when your server moves. The session's own start and last-activity timestamps are what the date range filters on; they are not breakdowns.
Pageview-level dimensions — Page Path, Page Number, Is Landing Page, Is Exit Page, Entry Type — belong to the pageview schema, and Goal Name, Goal Path and Goal Value to the goal schema. Neither set is selectable in Explore; goal dimensions drive the [Goals](/web-analytics/goals) tab.
## Beyond the Screens
The three schemas define more than the screens display, and the analytics query API can reach all of it — the same endpoint the console calls, documented in the [API Reference](/api-reference).
* **Sessions** also define Pageviews, Pages / Session, Avg Scroll Depth, Median Page Duration, Sessions with Goals, Goal Value and Identified Contacts.
* **Pageviews** define Page Views, Unique Pages, Median Page Duration, Median Scroll Depth, Entries, Exits and Exit Rate, along with the page dimensions above. No Web Analytics screen queries them today.
* **Goals** define Goals, Total Goal Value, Avg Goal Value, Median Goal Value and Converting Sessions.
Median Page Duration means different things on the two schemas. On sessions it is the median across visits of each visit's own median page time; the true per-page median is the identically named measure on the pageview schema.
## Custom Dimensions
Ten slots let you record something only you know about a visit — a plan tier, a test variant, a logged-in role.
Set them from your site by slot number:
```javascript theme={null}
NotifuseAnalytics.setDimension(1, 'pro')
NotifuseAnalytics.setDimensions({ 1: 'pro', 2: 'variant-b' })
```
They can also arrive in the URL: a link carrying `?custom_1=pro` fills slot 1 whenever the tracker initialises on that page, which is useful for tagging traffic you send yourself without touching the page's code.
The URL only fills slots that are still empty for the session. A value already set by `setDimension` — or by an earlier URL in the same visit — wins. Values are read once at initialisation, so a client-side route change does not re-read them.
Then give the slot a display name under **Settings** → **Web Analytics** → **Custom dimension labels**. The name is what appears in the reports; the slot keeps its `custom_1` identity underneath, so renaming it never rewrites your data.
Custom dimensions can also be *derived* rather than sent — an [attribution rule](/web-analytics/attribution) can write one from the referrer, a UTM value or a landing path, which is how you classify traffic you did not tag at the source.
A [backfill](/web-analytics/attribution#changing-rules-later) leaves custom dimensions alone on rows its rules do not write, so values your tracker sent survive it. Only Channel and Channel Group are cleared, because nothing but a rule can set those.
## Notes
* Values are capped at 256 bytes per custom dimension. The tracker's own check counts characters, so non-ASCII values should stay well under 256 characters or the server will reject the beat.
* CSV exports respect the filters and period currently applied, up to 1,000 rows.
* [API Reference](/api-reference)
# Setup
Source: https://docs.notifuse.com/web-analytics/setup
Install the tracking snippet and configure cookieless website analytics for your workspace.
Web Analytics records visits to your website in your own database, alongside your contacts. It is cookieless, stores no IP addresses, and needs no third-party service.
* **First-party**: sessions, pageviews and goals live in your workspace database, in three tables partitioned by month.
* **Cookieless**: no cookie is set and no identifier follows a visitor across sites. A session lives in `localStorage` and ends after 30 minutes of inactivity, or 24 hours after it started, whichever comes first.
* **Attribution built in**: 40 seeded rules classify traffic into channels, so you can see what your broadcasts actually brought in.
* **Connected to contacts**: a visitor you identify has their visits recorded on the contact timeline, where segments can reach them.
## Enable Web Analytics
Navigate to **Settings** → **Web Analytics**, turn it on, and add at least one domain.
Web Analytics cannot be enabled with an empty domain list. The list is what decides which sites may send you traffic, and it is also the list of hosts allowed to receive an identity from a tracked email link.
## Install the Snippet
Paste this before the closing `` tag of every page you want to track.
```html theme={null}
```
The settings screen shows this snippet with your workspace ID and endpoint already filled in — copy it from there rather than editing the example.
The script is served from your own instance at `/na.js` and cached for an hour. A hashed variant, `/na..js`, is served with a one-year immutable cache; use it if your CDN prefers permanent URLs. Both send beats to `POST /track`.
Anything the tracker cannot accept — a disabled workspace, a domain that is not on your list, a request that looks like a bot — is answered with a success response and discarded. The SDK never retries something that cannot succeed, so a misconfiguration shows up as missing data rather than as errors in your browser console.
## Allowed Domains
Each entry is a hostname, with one optional leading wildcard.
| Entry | Matches | Does not match |
| - | - | - |
| `example.com` | `example.com` | `www.example.com`, `shop.example.com` |
| `*.example.com` | `example.com`, `www.example.com`, `shop.example.com` | `example.org` |
Ports are not part of a hostname and must not appear. A wildcard over a single label — `*.com`, `*.io` — is refused: it would hand every recipient's identity to every link in that suffix.
Most sites want `*.example.com`. It covers the apex domain and every subdomain, so `www` keeps working when someone adds it later.
## Settings
| Setting | What it does |
| - | - |
| **Enable Web Analytics** | Starts accepting beats. Off by default. |
| **Allowed domains** | The hostnames that may send traffic, and the only hosts a tracked email link may hand an identity to. |
| **Bounce threshold** | Seconds of engaged time below which a visit counts as a bounce. Defaults to 10. |
| **Identify recipients who click a tracked email link** | Lets Notifuse add a signed identity to the links of tracked emails. Off by default — see [Identifying Visitors](/web-analytics/identifying-visitors). |
| **Custom dimension labels** | Display names for `custom_1` through `custom_10`. See [Reports](/web-analytics/reports). |
| **Geolocation** | Whether to resolve the visitor's country, and how precisely. |
The bounce threshold is about engaged time, not page count: a visitor who reads one long article is not a bounce, and one who opens five pages in four seconds is.
## Geolocation
Country is resolved from the visitor's IP address using a bundled MaxMind GeoLite2 database. **The IP address itself is never stored** — it is used for the lookup and discarded.
* **Store region** and **Store city** add those named levels. Turning them off drops the region and city *names*.
* **Coordinate precision** controls how much of the latitude and longitude is kept: `0` ≈ country (\~111 km), `1` ≈ regional (\~11 km), `2` ≈ city (\~1 km). It defaults to `2`.
A coordinate is a place name in another form, so it is never kept finer than the
finest name you store — whatever the precision setting says. With **Store city**
off, coordinates are capped at `1`; with both name toggles off, at `0`. The
setting is a ceiling that these toggles can lower, never raise.
Coordinates are made coarser rather than dropped, so the Live map keeps showing
where visitors are. An empty map would read as "nobody is online".
Self-hosted instances ship the database at `/app/geoip/GeoLite2-City.mmdb`. Point `GEOIP_DB_PATH` at another file to use your own.
## Permissions
Changing these settings requires `web_analytics` **write**. Reading the reports requires `web_analytics` **read**, and the **Web Analytics** section appears in the sidebar for a member holding either one.
The settings panel itself is not gated: any workspace member can open it and see the current configuration read-only, including the allowed domains.
## Notes
* Sessions, pageviews and goals are stored in three monthly-partitioned tables in your workspace database. There is no retention setting yet — nothing is deleted automatically.
* A session carries at most 1000 recorded actions. Beyond that the visit keeps being counted but new actions are dropped.
* Deleting a contact removes their address from every analytics row. The traffic itself remains, as anonymous.
* [API Reference](/api-reference)