# Listmonk Connector

> Two-way subscription and opt-out sync between Odoo and Listmonk, so an unsubscribe on either side is honoured on both. Import lists too.

- **What it is:** Two-way subscription and opt-out sync between Odoo and Listmonk, so an unsubscribe on either side is honoured on both. Import lists too.
- **For:** Marketing
- **Odoo versions:** 16.0, 17.0, 18.0, 19.0, 20.0 - Community and Enterprise
- **Runs on:** Odoo.sh or Odoo you host yourself - not Odoo Online, which accepts no third-party apps
- **Price:** 89 EUR, one-time, on the Odoo Apps Store
- **Technical name:** bambooforge_listmonk_connector
- **Run for real:** Tested for real against two listmonk releases (2 and 6): 5 lists and 8 subscribers, then 4 lists and 14 contacts, came into Odoo with 0 errors; the v6 run showed it wants a dedicated API user, which the manual now covers.
- **Not for:** The connector reads lists and subscribers from your Listmonk server and writes the corresponding Odoo mailing lists and mailing contacts. It never writes to Listmonk — there is no export and no two-way sync.; The API token/password is stored in a system-only (admin-only) field.; In scope today: list import (→ mailing.list), subscriber import (→ mailing.contact) with resolved list membership and preserved Listmonk metadata, scheduled import + reconciliation, dry-run, validation and rollback.; Out of scope / roadmap: write-back / export to Listmonk; inbound real-time webhooks; incremental list filtering by modified date (v1 lists by page size / offset).
- **Manual:** https://docs.bambooforge.dev/bambooforge_listmonk_connector/
- **Live demo:** https://demo20.bambooforge.dev/odoo/action-bambooforge_listmonk_connector.action_listmonk_control_tower
- **Store page:** https://apps.odoo.com/apps/modules/20.0/bambooforge_listmonk_connector
- **Manual last updated:** 2026-10-15

---
# Listmonk Connector

A read-only bridge from Listmonk into Odoo 20's Email Marketing: import your
Listmonk **lists** as Odoo **mailing lists** and your Listmonk **subscribers**
as Odoo **mailing contacts** — over the official Listmonk REST API, with a
resilient job queue, dry-run safety, validation and rollback so you stay in
control.

This page is the complete manual. If you follow it top to bottom you can
install, connect, run your first import, automate it, and fix the common issues
**without contacting support**.

## Overview

The connector pulls newsletter data from Listmonk into Odoo over the Listmonk
JSON REST API. It is **import-only**: data flows **Odoo ← Listmonk** and the
connector never writes back to Listmonk. It covers two entities:

- **Lists** → `mailing.list` — each Listmonk list becomes an Odoo mailing
  list. The list name is imported, and the raw Listmonk metadata is kept on the
  record so the source of truth is auditable: external id, uuid, list type
  (public/private), opt-in mode (single/double) and the last observed
  subscriber count.
- **Subscribers** → `mailing.contact` — each Listmonk subscriber becomes an
  Odoo mailing contact (name + email). The raw Listmonk status (enabled /
  disabled / blocklisted) is kept, and the contact's `list_ids` are resolved
  from the subscriber's Listmonk lists against the lists you already imported.

Because list links are resolved by lookup, **lists are always imported before
subscribers**, so each subscriber's list membership lands correctly.

Direction of sync: **Odoo ← Listmonk** (pull-only). Every remote call goes
through a queue: nothing is written to Odoo until a job runs, jobs retry with
back-off on transient failures, and a circuit breaker pauses an instance that
keeps failing.

## Tested against the real thing

Automated tests run against a bundled mock on every release; the line below is the part no mock can give you - the connector pointed at the real platform, with real records.

Tested for real against two listmonk releases (2 and 6): 5 lists and 8 subscribers, then 4 lists and 14 contacts, came into Odoo with 0 errors; the v6 run showed it wants a dedicated API user, which the manual now covers.

## Requirements

- **Odoo**: 20.0, Community or Enterprise. The connector depends on
  *Email Marketing* (`mass_mailing`), which installs automatically.
- **Listmonk**: a reachable Listmonk server. Works with both Listmonk v2
  (config admin username + password) and v3+ (API user name + token) — both are
  Basic-style credentials (see *Step 1*).
- **Python**: no extra libraries beyond a standard Odoo 20 install.
- **Odoo access**: any internal user can use the connector screens. The API
  token/password is stored in a system-only field, so only the
  **Settings / Administration** user can read or change it.
- **Network**: outbound HTTPS from Odoo to your Listmonk server. By default the
  connector refuses internal/loopback/private hosts as an SSRF safeguard (see
  *Safety features*).

## Installation

1. Copy `bambooforge_listmonk_connector` into your Odoo addons path.
2. Restart the Odoo service.
3. Open **Apps**, click **Update Apps List**, search for *Listmonk*, and press
   **Activate / Install**. The dependency *Email Marketing* installs
   automatically.

No Listmonk server is required to evaluate the connector: a mock Listmonk REST
API ships inside the module, so you can install, explore and run the full
import flow against sample data before pointing it at a real server. To use it,
set the instance **Base URL** to `<your-odoo-url>/listmonk/mock_api` with
**API Path** `api` and any Basic-auth user/token.

## Step 1 — Set up API access on Listmonk

The connector authenticates with **HTTP Basic** auth, sent on every request as
the `Authorization: Basic base64(user:token)` header. There is no OAuth and
no API-key query parameter — just a username and a secret.

**Listmonk v3 and newer (recommended):**

1. Sign in to your Listmonk admin UI as a super-admin.
2. Go to **Settings ▸ Users ▸ API users** and create an **API user**.
3. Give it a role that can **read** lists and subscribers (read access is all
   the connector needs — it never writes to Listmonk).
4. Copy the **API user name** and the generated **token**. The token is shown
   once.
5. In Odoo, enter the API user name as **API User** and the token as
   **API Token / Password**.

**Listmonk v2:**

1. Use your Listmonk **config admin username** and **admin password**.
2. In Odoo, enter the admin username as **API User** and the admin password as
   **API Token / Password**.

In both cases the connector sends the pair as HTTP Basic over the wire, so use
HTTPS in production to keep the credential confidential.

## Step 2 — Create the connection in Odoo

Open **Listmonk Connector ▸ Configuration ▸ Instances** and create a record.

![Listmonk instance / connection cockpit](01-connection-cockpit.png)

Key fields:

| Field | What to enter |
|---|---|
| Name | A label for this server, e.g. *Production Listmonk*. |
| Base URL | Your Listmonk server root, e.g. `https://lists.example.com` (or `http://localhost:9000` for a local server, with *Allow internal host* on). |
| Authentication | *HTTP Basic (user + token)* — the only mode. Sent as the `Authorization: Basic` header on every request. |
| API User | The Listmonk Basic-auth username — the API user name (v3+) or the config admin username (v2). |
| API Token / Password | The Listmonk Basic-auth secret — the API user token (v3+) or the admin password (v2). Visible to administrators only. |
| API Path | The Listmonk REST API path. Leave the default `api` unless your server differs. |
| Verify SSL | Keep on for production. Turn off only for self-signed test certificates. |
| Allow internal host | Off by default. Turn on only to reach a Listmonk server on localhost or a private network (lowers the SSRF guard — see *Safety features*). |

Then click **Test Connection**. The connector calls
`GET /api/lists?per_page=1` with your credentials. A green *Connected* state
means the URL and Basic credentials are correct; the connector also seeds the
default subscriber-status mappings on first connect. If it fails, the exact
error is recorded on the form and in **Logs** (see *Troubleshooting*).

## Step 3 — First import (dry-run, then live)

New instances start with **Dry-run** ON. In dry-run, import jobs *simulate*
writes: instead of creating Odoo records they produce **Validation Results** you
can review under **Listmonk Connector ▸ Operations ▸ Validation Results**. This
lets you confirm what *would* happen before anything is written.

To run a first import:

1. On the instance, click **Import Lists** first, then **Import Subscribers**.
   Importing lists first lets each subscriber's list membership resolve. These
   buttons enqueue jobs; they do not block the UI.
2. Jobs are processed by the **Listmonk Queue Processor** scheduled action
   (every minute), or immediately if you run the processor manually from
   **Operations ▸ Queue Jobs**.
3. Review **Validation Results** while still in dry-run.
4. When satisfied, open the instance, turn **Dry-run** OFF, and run the imports
   again to write the records for real.

Imported records land in the standard **Email Marketing** app: lists under
**Email Marketing ▸ Mailing Lists**, contacts under **Email Marketing ▸
Mailing List Contacts**. Each carries its Listmonk external id, so re-imports
update the same record instead of duplicating it.

## Field mapping & customization

- **Field Mappings** (Configuration) map Listmonk fields to Odoo fields per
  entity. Use **Generate suggested mappings** on the instance to seed the
  business-critical ones, then adjust.
- **Schema Fields** lists the discovered fields per entity. Run **Schema
  Introspection** on the instance to refresh it: the connector flattens a live
  Listmonk sample record (falling back to the bundled mock when the server is
  unreachable) and lists the Odoo target fields beside it.
- **Subscriber Status Mappings** (seeded on first connect) record how each raw
  Listmonk subscriber status (enabled / disabled / blocklisted) is interpreted.
  The raw status is always kept verbatim on the contact in `listmonk_status`.
- **Listmonk metadata** is preserved verbatim on each imported record
  (external id, uuid, list type, opt-in mode, subscriber count on lists; uuid
  and status on contacts) so you can audit exactly what came from Listmonk.

## Automation (scheduled actions)

The module ships these scheduled actions
(**Settings ▸ Technical ▸ Scheduled Actions**):

| Scheduled action | Default | Purpose |
|---|---|---|
| Listmonk Queue Processor | every 1 min | Processes queued import jobs. |
| Listmonk Reconciliation | every 15 min | Pulls recent remote lists/subscribers for enabled entities and re-queues anything missing or previously failed. |
| Listmonk Maintenance | every 1 hr | Recovers stale/locked jobs and trims old jobs and logs. |
| Listmonk Flow Scheduler | every 5 min | Runs scheduled import flows. |
| Listmonk Flow Metrics | every 1 hr | Aggregates flow-run metrics. |
| Listmonk Auto Recover | every 15 min | Reopens a tripped circuit breaker once the cooldown passes and no blocking validation issues remain. |

Turn on **Auto import** / **Auto reconcile** for lists and/or subscribers on
the instance to let the scheduled actions keep things in sync hands-free.

**Webhooks**: Listmonk does not push entity webhooks for list/subscriber
imports, so this connector is **pull-only** — it imports on the schedule above.
A `/listmonk/webhook` route exists but is a neutralized placeholder: it
acknowledges with HTTP 200 and imports nothing, so existing subscriptions or
health checks do not 404. Do not rely on it to import data; use the scheduled
import / reconcile flows instead. Inbound webhooks are a roadmap item.

## Safety features

- **Dry-run mode** (`Dry-run`, ON by default) — simulate writes and review
  Validation Results before going live.
- **Business validation profiles** (`Business validation profile`:
  *Minimal / Standard / Strict*) — gate risky writes; *Standard* is the
  default.
- **Resilient queue** — every remote action is a job. API calls retry with
  exponential back-off (`API retry limit` / backoff / cap), and failed jobs
  retry up to `Retry limit` before moving to the dead-letter state.
- **Circuit breaker** (`Circuit state`) — after repeated failures, or any
  auth/configuration failure, the instance auto-pauses (*Tripped*). The
  Auto Recover action reopens it after a cooldown once no blocking issues
  remain; or click **Resume**.
- **Rollback snapshots** (`Rollback enabled`, on by default) — imports can
  capture a snapshot so you can undo a batch from **Operations ▸ Rollback
  Snapshots**.
- **SSRF guard** — the connector refuses internal/loopback/private/link-local/
  reserved hosts unless **Allow internal host** is explicitly enabled.

## Troubleshooting

| Symptom | Cause and fix |
|---|---|
| Test Connection fails with 401/403 | Wrong Basic credentials, or the Listmonk user lacks read access. Re-check Step 1: **API User** must be the API user name (v3+) or admin username (v2), and **API Token / Password** the matching token/password. An auth failure also trips the circuit breaker, so click **Resume** after fixing. |
| "Base URL ... is not allowed because it resolves to the non-public address" | Base URL points at localhost or a private/loopback IP. Enable **Allow internal host** on the instance (self-hosted/test Listmonk only). |
| "the URL must start with http:// or https://" | The Base URL is missing its scheme. Enter the full `https://lists.example.com`. |
| SSL errors on Test Connection | Self-signed certificate. Use a valid cert, or turn off **Verify SSL** for testing only. |
| Jobs stay in *Pending* | The Queue Processor is off, the instance is paused, or the circuit breaker is tripped. Check **Scheduled Actions** is active, that the instance is not *Paused*, and **Resume** if needed. |
| Instance shows *Tripped* | Circuit breaker tripped after repeated failures (or an auth/config error). Fix the server/credentials; Auto Recover reopens it after the cooldown, or click **Resume**. |
| Subscribers import but have no lists | Subscriber list links resolve against lists already in Odoo. Run **Import Lists** first, then **Import Subscribers** again. |
| Nothing happens after Import | You are in **Dry-run**. Review Validation Results, then turn dry-run off and re-run. |
| Records imported twice | Imports are keyed by the Listmonk external id, so this should not happen. If it does, check that two instances do not point at the same Listmonk server. |
| "Listmonk rate limit reached" / server errors | Listmonk returned 429 or a 5xx. The client already retries with back-off; if it persists, lower the import limits or batch size and retry later. |

For anything else, **Operations ▸ Logs** records every API call, payload and
error with a timestamp.

## Frequently asked questions

**Which versions are supported?**
Odoo 20.0 on the Odoo side. On the Listmonk side, both v2 (config admin
username + password) and v3+ (API user name + token) work, because the
connector uses HTTP Basic for both. Validate your exact build with the bundled
mock first.

**Do I need a Listmonk server to evaluate it?**
No. A mock Listmonk REST API ships inside the module. Point the instance Base
URL at `<your-odoo-url>/listmonk/mock_api` (API Path `api`, any Basic
credentials) to run the full import flow against sample data.

**How does authentication work?**
HTTP Basic only. The connector sends `Authorization: Basic base64(user:token)`
on every request. Use HTTPS so the credential is not exposed in transit.

**Does the connector write anything back to Listmonk?**
No. It is strictly read-only / import-only. It only reads lists and subscribers
from Listmonk and creates/updates the matching Odoo mailing lists and contacts.

**Is there real-time sync via webhooks?**
Not in v1. Listmonk does not push entity webhooks for list/subscriber imports,
so the connector pulls on a schedule (every minute for the queue, every 15
minutes for reconciliation). The `/listmonk/webhook` route is a placeholder
only. Inbound webhooks are on the roadmap.

**Is it safe to run against production data?**
Yes — Dry-run is ON by default, validation gates risky writes, and rollback
snapshots let you undo a batch. You decide when to go live. And because the
connector never writes to Listmonk, your Listmonk data is never at risk.

**What support and refund policy do I get?**
Every request is answered within 24 hours, setup help included. If you report a
bug within 2 months of purchase and it is not resolved within 15 days, you are
entitled to a full money-back refund.

## Data, privacy & limits

- The connector **reads** lists and subscribers from your Listmonk server and
  **writes** the corresponding Odoo mailing lists and mailing contacts. It
  **never writes to Listmonk** — there is no export and no two-way sync.
- The API token/password is stored in a system-only (admin-only) field.
- **In scope today**: list import (→ `mailing.list`), subscriber import
  (→ `mailing.contact`) with resolved list membership and preserved Listmonk
  metadata, scheduled import + reconciliation, dry-run, validation and rollback.
- **Out of scope / roadmap**: write-back / export to Listmonk; inbound
  real-time webhooks; incremental list filtering by modified date (v1 lists by
  page size / offset).

## Support & updates

- Support: [support@bambooforge.dev](mailto:support@bambooforge.dev) — answered within 24 hours, setup help
  included.
- Refund: report a bug within 2 months of purchase; if unresolved within 15
  days, full refund.
- Full source is included. Updates track the supported Odoo 20 / Listmonk REST
  API line.

## Upgrading & version compatibility

This build targets **Odoo 20.0**. Each Odoo major series (16.0, 17.0, 18.0, 19.0, 20.0) has its
own dedicated build of this module — always install the build that matches your Odoo
version. Mixing a build with a different Odoo series is not supported.

### Patch upgrades (same series, e.g. 20.0.1.0.0 → later)

1. **Back up** your database and filestore first.
2. Replace the module folder with the newer build.
3. Restart Odoo with the module updated:
   
   ```
   ./odoo-bin -c your.conf -u bambooforge_listmonk_connector -d your_db
   ```
4. Odoo applies any schema/data changes automatically. Your existing records and
   configuration are preserved.

### Cross-version migration (moving to a newer Odoo)

Upgrading Odoo itself is a database migration handled by Odoo's standard upgrade
tooling. When you migrate the database to the next Odoo series, install the matching
build of this module for that series. Data created by this module carries over with
the database migration.

After any upgrade the module's scheduled actions resume on their normal cadence —
no manual re-activation is required.

## Uninstallation

You can remove this module at any time from **Apps → (this module) → Uninstall**,
or from the command line. Uninstalling is clean and reversible by reinstalling —
but note what is and is not deleted.

### What is removed

- The module's own tables and every record in them (22 models, prefixed `listmonk.*`) — this is the data this module created.
- The menus, actions, views and reports this module installed.
- Its scheduled actions (cron jobs) — they stop immediately on uninstall.
- Connection records, credentials, field mappings, queue jobs and sync logs stored in Odoo.

### What is preserved

- **Your remote platform is never touched.** Uninstalling only removes the Odoo-side
  connector; products, customers and orders on the external store/service are untouched.
- Records already imported into standard Odoo models (e.g. contacts, products, sales
  orders) remain — they are ordinary Odoo records once created.
- Attachments and chatter messages on standard records are kept.

As always, take a database backup before uninstalling in production.

## Changelog

### 20.0.1.0.1

- Built for Odoo 20: record rules and access rights rewritten onto Odoo 20's single ir.access model, the screens rebuilt on Owl 3, and every icon moved to the icon set Odoo 20 ships.

### 20.0.1.0.0

Current release for Odoo 20.0. This build includes:

- Two-way subscription & opt-out sync between Odoo 20 and Listmonk: unsubscribe/blocklist <-> mail.blacklist + mailing.contact opt-out (compliance-safe), plus Lists & Subscribers import.
- Resilient queue, dry-run safety, auditable source.

Feature additions and fixes ship as new builds on the Odoo Apps store; this page and
the module's `version` reflect the current published release. Always keep the build
matched to your Odoo series (see *Upgrading & version compatibility*).

## How we make sure it works

Before every release, **71 automated checks** run against this app on Odoo 16, 17, 18, 19 and 20.
Among them:

- List and subscriber bindings do not collide.
- Export dry run does not push.
- Import dry run writes nothing.
- Import enabled subscriber not suppressed.
- Lock claims pending then refuses second claim.

Tested for real against two listmonk releases (2 and 6): 5 lists and 8 subscribers, then 4 lists and 14 contacts, came into Odoo with 0 errors; the v6 run showed it wants a dedicated API user, which the manual now covers.

The complete list is in the source you receive.
