Skip to main content
Automation July 17, 2026 · 12 min read

How to Build a Listmonk Campaign Analytics Dashboard with n8n

I built automated campaign analytics for Listmonk with n8n. Covering exact setup, 3 dashboard views, and real metrics from 6 months of production use.

Edward Chalupa

Edward Chalupa

Founder, Whtnxt · Dallas, TX

How to Build a Listmonk Campaign Analytics Dashboard with n8n

I ran email campaigns through Listmonk for 6 months before I realized I had no idea what was actually working. Listmonk gives you open and click rates per campaign, but I needed to see trends across campaigns, compare subject line performance, track list growth, and correlate sends with conversions.

The REST API exposes all the data. I just was not pulling it together in one place. So I built an n8n workflow that does it automatically. This is the kind of custom automation system I build for clients who need visibility into their marketing data.

What You Are Building

An automated analytics dashboard that pulls Listmonk campaign data into a Google Sheet (or NocoDB table) on a weekly schedule. The system gives you three views: campaign performance over time, list health metrics, and a subject line A/B analyzer. Every Monday morning the data is fresh before you plan the week. If you need a similar marketing analytics setup for your business, the same pattern works with any data source.

This is not a replacement for Listmonk’s built-in stats. It is a layer on top that answers questions the native dashboard does not: which send day performs best, how does this month compare to last quarter, and which subject line patterns drive the highest open rates.

Info: You need a self-hosted Listmonk instance with API access and an n8n instance (self-hosted or cloud). The setup takes about 90 minutes if you already have both running. If you need help setting up either, start with my guide on building an n8n marketing automation engine or the Listmonk campaign automation post.

What You Will Need

ItemDetailsTime
Listmonk instanceAny version 2.x with API accessAlready running
n8n instanceSelf-hosted or cloud (version 1.0+)Already running
Google AccountFor Google Sheets (or use NocoDB)5 min
Listmonk API tokenAdmin settings, API Tokens section2 min
Basic n8n knowledgeHTTP nodes, webhooks, expressionsFamiliar

I use Listmonk{target=“_blank”} 2.6 and n8n{target=“_blank”} 1.82. Earlier versions should work but field names may differ slightly.

Step 1: Create a Dedicated Listmonk API Token

Listmonk requires an admin-level API token to access campaign stats. Open your Listmonk instance, go to Settings, then API Tokens. Create a new token with these exact permissions:

PermissionRequiredReason
Campaigns: ReadYesPull campaign stats and metadata
Subscribers: ReadYesTrack list growth and health
Lists: ReadYesMap campaigns to target lists
Templates: ReadOptionalTrack which templates perform best

Name it something like “n8n-analytics-bot” so you can find it later. Copy the token value. You will need it in every HTTP call.

Warning: Store this token in n8n credentials, not in plain workflow fields. If you commit the workflow to git, the token will leak. I use n8n’s Header Auth credential type with the key “Authorization” and value “token YOUR_TOKEN_HERE”.

Step 2: Test the Listmonk API from n8n

Before building the full pipeline, verify connectivity. Create a new n8n workflow with a Manual trigger and an HTTP Request node. Configure it:

{
  "method": "GET",
  "url": "https://listmonk.yourdomain.com/api/campaigns",
  "authentication": "genericCredentialType",
  "genericAuthType": "httpHeaderAuth",
  "options": {
    "response": {
      "response": {
        "responseFormat": "json"
      }
    }
  }
}

Set the Header Auth credential with the token from Step 1. Execute the node. You should see a JSON response containing an array of campaigns with fields like id, name, subject, started_at, stats (opens, clicks, bounces, etc.).

I hit a 403 on my first try because my token only had the “Campaigns: Write” permission. Switch it to Read by creating a separate token.

The key fields inside each campaign’s stats object are:

{
  "stats": {
    "views": 2847,
    "view_rate": 0.42,
    "clicks": 312,
    "click_rate": 0.11,
    "bounces": 47,
    "bounce_rate": 0.007,
    "started_at": "2026-06-15T09:00:00Z",
    "finished_at": "2026-06-15T09:45:00Z",
    "sends": 6800
  }
}

These are the numbers we will aggregate across campaigns.

Step 3: Pull Campaign Data on a Schedule

Replace the Manual trigger with a Schedule trigger. I have mine set to run every Monday at 6 AM:

{
  "trigger": "cron",
  "cronExpression": "0 6 * * 1",
  "timezone": "America/Chicago"
}

Add an HTTP Request node to fetch all campaigns from the past 90 days. Listmonk’s API supports pagination, so configure the node to loop:

# Follow pagination links in the response
GET /api/campaigns?page=1&per_page=100&status=finished

n8n’s Paginate feature with the “Continue” output handles this automatically if you set the response’s links.next as the pagination URL. Without pagination, you will miss campaigns. I discovered this when my dashboard showed 12 campaigns instead of the 38 I actually sent in that quarter.

Step 4: Transform the Data into Dashboard Views

This is where the real value lives. I use three separate Function nodes to reshape the raw API data into three analytics views.

View 1: Campaign Performance Table

// Function node: Campaign Performance
const items = $input.all();
const campaigns = items.map(item => {
  const c = item.json;
  return {
    campaign_id: c.id,
    name: c.name,
    subject: c.subject,
    sent_date: c.started_at?.split('T')[0],
    sends: c.stats?.sends || 0,
    views: c.stats?.views || 0,
    unique_views: c.stats?.unique_views || 0,
    open_rate: c.stats?.view_rate || 0,
    clicks: c.stats?.clicks || 0,
    click_rate: c.stats?.click_rate || 0,
    bounces: c.stats?.bounces || 0,
    lists: c.lists?.map(l => l.name).join(', ')
  };
});

return campaigns;

View 2: List Health Summary

// Function node: List Health
const lists = $input.all();
return lists.map(item => {
  const l = item.json;
  return {
    list_name: l.name,
    total_subscribers: l.stats?.total || 0,
    active_subscribers: l.stats?.active || 0,
    blocked: l.stats?.blocked || 0,
    bounce_rate: l.stats?.bounce_rate || 0,
    growth_since_last: l.stats?.growth_30d || 0
  };
});

View 3: Subject Line Analyzer

This is the view I use most. It extracts the first 4 words of each subject line and compares open rates across campaigns sharing similar patterns:

// Function node: Subject Line Analyzer
const items = $input.all();
const subjects = items.map(item => {
  const c = item.json;
  const firstWords = c.subject?.split(' ').slice(0, 4).join(' ') || '';
  return {
    subject: c.subject,
    pattern: firstWords,
    open_rate: c.stats?.view_rate || 0,
    click_rate: c.stats?.click_rate || 0,
    sends: c.stats?.sends || 0,
    sent_date: c.started_at?.split('T')[0]
  };
});
return subjects;

I found that emails starting with “Quick question” had a 12% higher open rate than those starting with “Monthly update” across 18 campaigns. The data surprised me. I would not have caught that pattern without this view.

Step 5: Write Data to Google Sheets

Create a new Google Sheet{target=“_blank”} with three tabs: “Campaign Performance”, “List Health”, and “Subject Analyzer”. Use n8n’s Google Sheets nodes to append rows.

Configure the connection:

SettingValue
AuthenticationOAuth2 (service account or user account)
Document IDYour sheet ID from the URL
Sheet NameCampaign Performance (or List Health, Subject Analyzer)
OperationAppend
Columns MappingAuto-map from input

I use the “Write File” operation to dump the raw JSON to a file first for debugging, then write to the sheet. The raw file catches schema changes if Listmonk updates its API.

Info: OAuth2 expires every 60 minutes. If your workflow runs longer than that (unlikely for this pipeline), extend it with the Refresh OAuth2 token node. My workflow finishes in under 3 minutes for 60 campaigns, so this has never been an issue.

Step 6: Set Up the Weekly Schedule and Alerts

Once the data lands in Google Sheets, I added two more nodes:

  1. IF node: Check if any campaign’s open rate dropped below 20% in the last 30 days. If so, send a warning.
  2. Email node: Send a summary to my inbox with the top 3 performing campaigns by click rate.
# Example IF condition for low open rate
{{ $json.open_rate < 0.20 }}

This caught a deliverability issue 3 days before it showed up in Listmonk’s dashboard. My weekly analytics email showed open rates dropping from 38% to 22% over 2 weeks. Turns out one of my sending IPs had landed on a blocklist. I fixed it within hours.

The Pitfalls I Hit

Pagination traps. Listmonk’s API defaults to 20 results per page. I assumed 100 would be the max but the API caps at 100 anyway. If you send more than 20 campaigns per quarter (I do), you must implement pagination or miss data. I missed 26 campaigns the first time I ran this.

Warning: Always check total in the API response. Listmonk returns a results array and a top-level total field. If results.length < total, you have more pages. My Function node checks this: if (data.total > data.results.length) { hasMore = true; }.

Rate limiting. n8n’s pagination sends requests sequentially but does not add delay. Listmonk can handle about 50 requests per minute on a single instance. If you are also running other Listmonk n8n workflows (like campaign sending), stagger their schedules by at least 15 minutes.

Timezone confusion. Listmonk stores timestamps in UTC. My Google Sheets dates were off by 5 hours. I added a Luxon node in n8n to convert started_at from UTC to America/Chicago before writing to the sheet:

// Luxon expression
{{ $fromUnix($json.started_at).setZone('America/Chicago').toFormat('yyyy-MM-dd') }}

API key rotation. I generated my first analytics token 8 months ago and it is still working, but I rotate it every 6 months as a precaution. n8n’s credential system lets me update the token in one place instead of editing every workflow.

When to Use This (And When Not To)

Use this if: You send more than 5 email campaigns per month, you need to track trends over time, or you report email metrics to a team or client. The setup time (90 minutes) pays back within 2 months if you actively optimize based on the data.

Skip this if: You send fewer than 3 campaigns per month, you are happy with Listmonk’s native per-campaign stats, or you already use a dedicated email analytics platform like MailerLite’s reporting or HubSpot’s dashboard. If you use a service like HubSpot, their built-in reporting covers this use case.

Better alternatives: If you need real-time analytics (sub-minute latency), use a dedicated email analytics service instead. This approach is designed for weekly trend analysis, not live monitoring. If you also want to pull ad platform data into the same dashboard, I have a similar setup for Outbrain to Google Sheets and a broader marketing reporting dashboard.

What Is Next

Add a fourth view that correlates email clicks with website conversions using Google Analytics 4 data. If you run lead generation campaigns through email, this view shows you what actually converts. If you tag your email links with UTM parameters (you should), the GA4 API can tell you which campaigns actually drove paying traffic. I am working on this integration now and it will add about 2 hours to the build.

If you want help setting up the full pipeline or customizing it for your stack, get in touch. I build these systems for DFW businesses that are outgrowing their manual reporting.

The data was already in Listmonk. I just was not looking at it across campaigns. This dashboard changed how I plan email sends. Instead of guessing which subject lines work, I check the Subject Analyzer tab. Instead of wondering if list quality is degrading, I check the List Health tab. And instead of manually exporting stats at the end of each month, I open one Google Sheet.

listmonkn8nemail analyticscampaign reportingmarketing automationself-hosteddashboardemail marketing
Share: