# 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" # the first key when SECRET_KEY lists several
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: it disappears from `templates.list` and can no longer be updated, but every version is kept, because messages already sent (message history, broadcast reports) are rendered from them. `templates.get` still returns it, with `deleted_at` set. Its id stays reserved: creating a template with the same id is refused.
# 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:** This endpoint subscribes contacts to **public lists** only. To add a contact to a private list, use the authenticated `/api/lists.subscribe` endpoint with a bearer token. A valid `email_hmac` in the contact object only lets the contact confirm a pending double opt-in on a private list: it never adds them to one, nor back to one they left.
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. The email's `editor_mode` must stay the one the template was created with; to switch between the visual editor and MJML code, create a new template. A deleted template cannot be updated.
# 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
> [!NOTE]
>
> **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
> [!WARNING]
>
> 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
> [!NOTE]
>
> 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.
> [!NOTE]
>
> 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.
> [!NOTE]
>
> The email assistant is available in the visual editor only. Templates written in MJML code mode don't show the panel, 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.
> [!WARNING]
>
> 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.
> [!TIP]
>
> 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.
> [!WARNING]
>
> 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).
> [!NOTE]
>
> **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.joinInvitation`, `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/audit-logs/list-audit-logs), [`auditLogs.get`](/api-reference/audit-logs/get-an-audit-log-entry), [`auditLogs.actions`](/api-reference/audit-logs/list-audit-actions) and [`auditLogs.export`](/api-reference/audit-logs/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/audit-logs/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 |
> [!TIP]
>
> 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 |
> [!TIP]
>
> 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.
> [!NOTE]
>
> 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.
> [!WARNING]
>
> Email nodes require a list to be selected on the automation. This ensures contacts have proper subscription context.
The automation's list doesn't limit who receives its emails. A marketing email is skipped only for contacts who unsubscribed, bounced or complained on that list, and contacts with no subscription to the list receive it too. Its unsubscribe link records them on the list as unsubscribed, so later marketing emails from the automation exit them. To email only the list's subscribers, put a [List Status Branch](#list-status-branch-node) before the email node and leave its **Not in List** path unconnected, which ends the automation for them.
#### 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.
The receiver 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. The editor marks a webhook node whose URL names such an address, and saving the automation is refused until it is fixed. A step refused this way is retried like any other failed step, and then fails the contact's journey.
> [!NOTE]
>
> Self-hosted only: if you run Notifuse yourself and deliberately post 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 edit automations.
## 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).
> [!WARNING]
>
> **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).
> [!NOTE]
>
> 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 a retry with exponential backoff: 2 minutes after the first failure, then 4 minutes
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.
> [!WARNING]
>
> **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.
> [!NOTE]
>
> **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
## Sending Speed
Broadcasts and automation emails go out through the email queue, at the **Rate Limit for Marketing** set on each email integration, in emails per minute.
* **Set the limit to what your provider accepts, minus your other traffic on the same account.** Transactional emails are sent as soon as the API is called and don't count against this limit, but when they go through the same provider account they use the same quota. So do other integrations on that account, in this workspace or another: their limits add up. Amazon SES states its quota per second: 100 emails per second is 6,000 per minute.
* **The limit is a maximum, and it applies per Notifuse instance.** Several instances sharing one database each apply it on their own. While the limit is what holds them back, they compete for the same emails and together send at about the limit; when it is set above what one instance can reach (see below), their rates add up and can exceed it. Below the limit, an instance's pace is set by the points that follow.
* **Emails sent at once.** Each email integration sends up to 10 emails at once, and SMTP integrations one at a time: each SMTP email opens its own connection, and many SMTP servers refuse several at once. An integration can't go faster than that number divided by the time one email takes, which is the provider's answer plus about three queries on the workspace's database: 10 at once at 100 ms each is 6,000 per minute, and one at a time at 300 ms each is 200 per minute. A self-hosted instance can raise `EMAIL_QUEUE_CONCURRENCY`, or `EMAIL_QUEUE_SMTP_CONCURRENCY` if its SMTP server accepts several connections (see [Installation](/self-hosting/installation#optional-variables)). That helps when the provider's answer is what takes time. When the database's queries are, with a distant database for instance, the workspace's few connections, shared with the API (`DB_MAX_CONNECTIONS_PER_DB`, 3 by default), are what hold sending back.
* **Rounds.** An integration sends at most 200 emails per round, and a round starts at most once a second, so it never goes above about 12,000 per minute per instance. A round ends when every integration on the instance has sent its emails for it. While another integration is sending as fast as its own limit allows, a round takes about two seconds, or less when that limit is above 6,000 per minute. This brings the ceiling down to 6,000 per minute, or to that integration's limit when it is between 6,000 and 12,000. Rounds take longer when an integration's emails are slow to send.
* **Workspaces.** An instance sends for five workspaces at a time. With more than five sending at once, they take turns, and each stays below its limit.
A limit above what the provider accepts makes it refuse emails. A refused email is retried one minute later, then two minutes after that, and given up at its third refusal; retrying the broadcast sends it again. Several refusals in a row pause the integration's broadcasts (see below), and its other queued emails, such as automation emails, wait for the cooldown. The integration then sends one of them as a test and goes back to full speed if it goes through. A broadcast running an A/B test is paused only once all its emails are queued; until then, its emails wait like the others. With Amazon SES, the client first retries a refused email for several seconds, which holds up the round for the other integrations on the instance.
## 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; those the provider refused for good, or on their last attempt, are given up instead. On a single Notifuse instance, an email still being sent when it triggers is paused with the rest if the provider fails it, unless it was on its last attempt; one refused for its recipient is given up as usual.
* **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.
> [!WARNING]
>
> 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`.
> [!WARNING]
>
> 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.
### Deleting a Contact
Deleting a contact with `contacts.delete` also deletes its list subscriptions, unsubscribed, bounced and complained statuses included, so Notifuse no longer remembers that the address opted out. A later import, upsert with `subscribe_to_lists` or `lists.subscribe` adds the address back as a new, subscribed contact. To keep honouring an opt-out, set the contact's list status to unsubscribed instead of deleting it.
An unsubscribe link in an email sent before the deletion still answers successfully, but stores nothing about the address.
### 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`
> [!NOTE]
>
> 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": ["*"]
}
]
```
> [!TIP]
>
> 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:
> **[Self-Hosted S3 Storage](https://github.com/Notifuse/selfhost_s3)**
>
> 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** | ❌ Never receives emails | Email address is invalid or unreachable |
| **Complained** | ❌ Never receives emails | Marked emails as spam |
> **Important**: Broadcasts leave out the list's unsubscribed contacts unless the broadcast sets `exclude_unsubscribed` to `false`, for example for a legal notice. Leaving `exclude_unsubscribed` out of an API request means exclude them, as the console does. Bounced and complained contacts are never emailed, whatever that setting.
A contact who unsubscribes from an email sent for a list they weren't on, such as an automation email, is recorded on that list as **unsubscribed**. An unsubscribe never replaces a **bounced** or **complained** status.
## 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**: ❌ Not offered to contacts who aren't on them. A contact sees a private list they are active or pending on, and can only leave it there.
* **Subscriber Access**: Only the workspace adds contacts, from the console or the authenticated API (`lists.subscribe`). A contact can't join a private list from the notification center or the public `/subscribe` endpoint, nor come back after leaving one. Adding them back is up to the workspace. A link from their emails only lets them confirm a pending double opt-in.
* **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.
> [!WARNING]
>
> **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 each event's data exactly as the email provider sent it. When a provider sends its events in batches (SendGrid, SparkPost, Mailjet), each event keeps its own entry of the batch
* **Real-Time Processing**: Webhooks processed immediately upon receipt
* **Provider Integration**: Data from Mailgun, Amazon SES, Postmark, etc.
* **Emails Notifuse Sent**: Only events about an email Notifuse sent, for the address it went to, are listed and acted on. Events about other mail, such as another application's on the same provider account or an integration's test email, are ignored.
### 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)
* Polish (pl)
Defaults to English if browser language is not supported.
When the contact clicks, taps or presses a key on the page, their browser's timezone is saved to their profile, and so is their browser's language if their profile has none. Opening the page alone saves nothing, so email security scanners that open links in a browser don't change contact data.
### Subscription Management
Users can:
* View all public lists, and the private lists they are active or pending on
* Subscribe/unsubscribe with one click
* Leave a private list, which then disappears from their preferences. Only the workspace can add them back (see [Private Lists](/features/lists#private-lists)).
* See real-time feedback messages
### Unsubscribe from the Mail Client
Broadcasts, and emails sent by automations attached to a list, carry the `List-Unsubscribe` and `List-Unsubscribe-Post` headers ([RFC 8058](https://www.rfc-editor.org/rfc/rfc8058)), so Gmail, Yahoo, Outlook and other mail clients can show their own **Unsubscribe** button next to the sender:
* **One-click**: when the mail client offers one-click unsubscribe, the contact is unsubscribed from the list right away. The contact's click on the mail client's button is the confirmation.
* **Opened in a browser**: mail clients that don't offer one-click open the link instead. The notification center then shows the list and asks the contact to confirm, because anti-spam filters also open these links on their own.
Mail clients only offer one-click when the email's DKIM signature covers both headers, and that signature is applied by your email provider.
## Configuration
### Branding
The notification center uses:
* Logo from workspace settings
* Website URL from workspace settings
## Technical Details
### User Flow
What happens depends on the link the contact followed:
* **Unsubscribe link in the email** (`{{ unsubscribe_url }}`): the contact is unsubscribed from that list as soon as the page opens, and the page confirms it. They can then manage their other lists.
* **The mail client's Unsubscribe button**: unsubscribes in one click, or opens a page that asks the contact to confirm, as described in [Unsubscribe from the Mail Client](#unsubscribe-from-the-mail-client).
* **Preferences link** (`{{ notification_center_url }}`): shows the contact's lists, and each subscribe or unsubscribe applies 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
## Editor modes
When you create a template, you choose how its email is written:
* **Visual**: the drag-and-drop editor, where blocks, styles and Liquid variables are edited in place.
* **Code (MJML)**: you write the MJML yourself, with Liquid, in a code editor with a live preview.
The editor mode is set when the template is created and can't be changed afterwards. To move a design to the other editor, create a new template in that mode and copy the content across with the **Import / Export** menu of each editor:
* **Code to visual**: in the code template, use **Export MJML**; in the new visual template, use **Import → MJML** with that file.
* **Visual to code**: in the visual template, use **Export → MJML**; in the new code template, use **Import MJML** with that file.
Check the preview after copying. The visual editor refuses MJML components it doesn't support, and Liquid placed between blocks (for example a `{% for %}` loop around sections) is not kept when importing into it. In the other direction, write Liquid inside links with single quotes (`{{ link | append: '?ref=mail' }}`): double-quoted text there doesn't survive the export.
The new template has its own ID. Point your broadcasts, transactional notifications and automations at it, then delete the old one.
## 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

## Deleting a template
Deleting a template removes it from the template list and from every template picker, but Notifuse keeps it, with all its versions: messages already sent were rendered from them, and the message history and broadcast reports still display them.
Because of this, a deleted template keeps its ID. Creating a new template with the same ID is refused, and the API's `templates.update` answers `404` for it. Give the new template another ID.
Templates managed by an integration, such as the [Supabase](/integrations/supabase) auth emails, can't be deleted.
## 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.
> [!NOTE]
>
> 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.
> [!NOTE]
>
> **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.
> [!NOTE]
>
> Liquid is evaluated in the template you write, not in your data. A value coming from a contact field, a data feed or an API call is inserted as text: if it happens to contain `{{ ... }}` or `{% ... %}`, that text is shown as-is, not run as Liquid. Put merge tags in the template; feed the data plain values.
### 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
```
Opening this link unsubscribes the contact from the list right away and shows the [notification center](/features/notification-center). Mail clients can also show their own Unsubscribe button, from the `List-Unsubscribe` header that Notifuse adds to every email sent to a 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 |
> [!WARNING]
>
> 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.
> [!NOTE]
>
> 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 or edit automations. The same setting covers [automation webhook nodes](/features/automations#webhook-node).
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.
> [!NOTE]
>
> **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.
> [!WARNING]
>
> 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.
> [!NOTE]
>
> **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:
> **[Workspaces](/features/workspaces)**
>
> Isolated environments for organizing your email operations by project, client, or business unit.
> **[Contacts & Lists](/features/contacts)**
>
> Manage your subscriber database with powerful segmentation and list management tools.
> **[Email Templates](/features/templates)**
>
> Create reusable email designs with our visual builder and template system.
> **[Broadcast Campaigns](/features/broadcast-campaigns)**
>
> Send targeted marketing emails to specific segments of your audience.
> **[Transactional API](/features/transactional-api)**
>
> Send automated emails triggered by user actions or system events.
> **[Custom Events & Goals](/features/custom-events)**
>
> Track user behavior and conversions to build RFM segments.
> **[Web Analytics](/web-analytics/setup)**
>
> Cookieless website analytics with goals, attribution and annotations.
> **[Blog](/blog/setup)**
>
> Full-featured blogging system with Liquid theming.
> **[AI Assistant](/features/ai-assistant)**
>
> Write posts, design emails and query your analytics, on your own AI provider key.
> **[Notification Center](/features/notification-center)**
>
> Embedded notification system for your applications.
> **[Audit Logs](/features/audit-logs)**
>
> 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:
> **[Amazon SES](/integrations/amazon-ses)**
>
> 
>
> Cost-effective email service from AWS
> **[Mailgun](/integrations/mailgun)**
>
> 
>
> Powerful email API for developers
> **[Postmark](/integrations/postmark)**
>
> 
>
> Fast and reliable transactional email
> **[SendGrid](/integrations/sendgrid)**
>
> 
>
> Reliable email delivery at scale
> **[SparkPost](/integrations/sparkpost)**
>
> 
>
> Enterprise email delivery platform
> **[Mailjet](/integrations/mailjet)**
>
> 
>
> All-in-one email service solution
> **[SMTP](/integrations/smtp)**
>
> Send emails via your own SMTP server
## Platform Integrations
Seamlessly integrate Notifuse with your existing tools and workflows:
> **[Anthropic](/integrations/anthropic)**
>
> 
>
> Claude models for the AI assistant
> **[OpenAI](/integrations/openai)**
>
> 
>
> GPT models, or any OpenAI-compatible API
> **[Google Gemini](/integrations/gemini)**
>
> 
>
> Gemini models for the AI assistant
> **[Firecrawl](/integrations/firecrawl)**
>
> 
>
> Web access for AI research and content
> **[Supabase](/integrations/supabase)**
>
> 
>
> Auth emails and user sync integration
## Getting Started
> **[Notifuse Cloud](https://cloud.notifuse.com)**
>
> Get started in minutes with our managed cloud platform — from \$16/month.
> **[API Reference](/api-reference/introduction)**
>
> Explore our comprehensive API documentation and start integrating.
> **[Live Demo](https://demo.notifuse.com/console/signin?email=demo@notifuse.com)**
>
> 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.
> [!NOTE]
>
> **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.
> [!NOTE]
>
> **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.
> [!NOTE]
>
> 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.
> [!NOTE]
>
> 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.
> [!WARNING]
>
> **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/
```
> [!NOTE]
>
> 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.
> [!NOTE]
>
> 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.
> [!WARNING]
>
> **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 |
> [!WARNING]
>
> 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 |
> [!NOTE]
>
> 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
> [!NOTE]
>
> API calls to Claude count toward your Anthropic account usage. Monitor your usage in the Anthropic console to manage costs.
> [!TIP]
>
> 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` |
> [!NOTE]
>
> 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
> [!WARNING]
>
> Each web search or URL scrape counts toward your Firecrawl API quota. Monitor your usage in the Firecrawl dashboard to manage costs.
> [!TIP]
>
> 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 |
> [!WARNING]
>
> 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
> [!NOTE]
>
> API calls to Gemini count toward your Google AI usage. Monitor your usage in [Google AI Studio](https://aistudio.google.com) to manage costs.
> [!NOTE]
>
> 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.
> [!TIP]
>
> 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.
> [!NOTE]
>
> 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.
> [!NOTE]
>
> 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.
> [!NOTE]
>
> 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.
> [!TIP]
>
> Browse available models at [openrouter.ai/models](https://openrouter.ai/models) to find the best fit for your use case and budget.
> [!WARNING]
>
> 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
> [!NOTE]
>
> API calls to GPT models count toward your OpenAI account usage. Monitor your usage in the OpenAI dashboard to manage costs.
> [!TIP]
>
> 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
```
> [!WARNING]
>
> **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
```
> [!WARNING]
>
> 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 |
> [!WARNING]
>
> 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)
> [!WARNING]
>
> **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".
> [!NOTE]
>
> **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`).
> [!TIP]
>
> 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"
}
```
> [!WARNING]
>
> **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 |
> [!NOTE]
>
> 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
> [!NOTE]
>
> 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 |
> [!NOTE]
>
> 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.
> [!NOTE]
>
> **The key's address is derived from your label, not typed by you.** Notifuse builds it as `zapier-