Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" name: listmonk description: > Listmonk Email API Client for Python. Use when writing Python code that uses the listmonk package. license: MIT compatibility: Requires Python >=3.10.
Listmonk
Listmonk Email API Client for Python
Installation
pip install listmonk
When to use what
| Need | Use | |
|---|---|---|
| Look up a single subscriber | subscriber_by_email(email) / subscriber_by_id(id) / subscriber_by_uuid(uuid) | |
| Filter subscribers by a custom attribute | subscribers(query_text="subscribers.attribs->>'city' = 'Portland'") | |
| Unsubscribe someone but keep their record | block_subscriber(subscriber) | |
| Erase a subscriber entirely | delete_subscriber(email) | |
| Send a one-off email (reset code, receipt) | send_transactional_email(email, tx_template_id, template_data={...}) | |
| Attach a file to a campaign | upload_media(path) then create_campaign(..., media_ids=[m.id]) | |
| Schedule a campaign for later | create_campaign(..., send_at=datetime.now() + timedelta(hours=1)) |
API overview
Configuration & Authentication
Point the client at your Listmonk instance and authenticate.
set_url_base: Set the base URL of your Listmonk instance for all subsequent callsget_base_url: Return the configured base URL of your Listmonk instancelogin: Log into Listmonk and cache the credentials for the life of your appverify_login: Verify that the stored login credentials are still valid at the serveris_healthy: Check whether the server is reachable and the stored credentials are valid
Mailing Lists
Read and manage mailing lists.
lists: Get all mailing lists on the serverlist_by_id: Get the full details of a single mailing list by its IDcreate_list: Create a new mailing list on the serverupdate_list: Update an existing mailing list on the serverdelete_list: Delete a mailing list by its ID
Subscribers
Create, query, update, and manage the status of subscribers.
subscribers: Get the list of subscribers matching the given criteria, or all subscribers if no criteria are givensubscriber_by_email: Retrieve a single subscriber by their email address (e.g. "some_user@talkpython.fm")subscriber_by_id: Retrieve a single subscriber by their numeric Listmonk ID (e.g. 201)subscriber_by_uuid: Retrieve a single subscriber by their UUID (e.g. "c37786af-e6ab-4260-9b49-740adpcm6ed")create_subscriber: Create a new subscriber on the Listmonk serverupdate_subscriber: Update many aspects of a subscriber: email and name, custom attribute data, list membership, and statusadd_subscribers_to_lists: Add a number of subscribers to a number of lists in a single bulk operationenable_subscriber: Set a subscriber's status to enabled so they will receive campaignsdisable_subscriber: Set a subscriber's status to disabled, pausing their subscription so they will not receive campaignsblock_subscriber: Add a subscriber to the blocklist, effectively unsubscribing them so they will not receive any mailconfirm_optin: Confirm a subscriber's opt-in to a list via the APIdelete_subscriber: Completely delete a subscriber from your system (as if they were never there)
Campaigns
Create, preview, update, and delete email campaigns.
campaigns: Get all campaigns on the servercampaign_by_id: Get the full details of a campaign with the given IDcampaign_preview_by_id: Get the rendered preview of a campaign with the given IDcreate_campaign: Create a new campaign with the given parametersupdate_campaign: Update an existing campaign with the provided campaign informationdelete_campaign: Completely delete a campaign from your system
Media
Upload files to the media library to attach to campaigns.
upload_media: Upload a file to the Listmonk media library
Templates
Manage email templates and set the default.
templates: Retrieve all templates defined on the Listmonk instancetemplate_by_id: Retrieve a single template by its numeric IDtemplate_preview_by_id: Render and return a preview of a templatecreate_template: Create a new template on the Listmonk instanceupdate_template: Update an existing template on the Listmonk instanceset_default_template: Mark the given template as the default for its typedelete_template: Permanently delete a template from the Listmonk instance
Transactional Email
Send one-off transactional messages.
send_transactional_email: Send a transactional email through Listmonk to a single recipient
Data Models
Pydantic models returned by and passed to the API functions.
models.MailingListmodels.SubscriberStatusmodels.SubscriberStatusesmodels.Subscribermodels.CreateSubscriberModelmodels.Campaignmodels.CreateCampaignModelmodels.UpdateCampaignModelmodels.CampaignPreviewmodels.Templatemodels.CreateTemplateModelmodels.TemplatePreviewmodels.Media
Exceptions
Errors raised by the client.
errors.ValidationErrorerrors.OperationNotAllowedErrorerrors.ListmonkFileNotFoundError
Gotchas
- Call set_url_base() then login() before any other function, or every data call raises OperationNotAllowedError.
- login(), is_healthy(), and verify_login() return False (they do not raise) when credentials are rejected or the server is unreachable — check the bool.
- Timeouts use httpx2 (a fork of httpx), not httpx: build them with httpx2.Timeout(...) and catch httpx2.HTTPStatusError.
- Every template body must contain the Go-template placeholder {{ template "content" . }} exactly once, or create_template() raises ValueError. It is Go template syntax, not Jinja.
- update_campaign() replaces the whole attachment set: media_ids=None re-sends the campaign's existing media, media_ids=[] clears them, and a new list swaps them. A past send_at is silently dropped to None.
- subscribers(query_text=...) requires the subscribers:sql_query permission on the user's role, or the server returns HTTP 403.
- send_transactional_email() requires the recipient to already be a subscriber, and template_id must reference a 'tx' template, not a 'campaign' template.
- Auth is module-level global state: only one instance can be targeted at a time and credential changes are not thread-safe.
- Custom email headers are a list of single-entry dicts (e.g. [{'X-Priority': '1'}]), not one dict.
Best practices
- Configure set_url_base() then login() once at startup and check login()'s bool return before proceeding.
- Update objects by fetching the model, mutating fields in place, then passing it back to update_subscriber/update_campaign/update_template — the client re-fetches and returns the server's fresh copy.
- Use block_subscriber() to unsubscribe someone while keeping their record; use delete_subscriber() to erase them entirely.
- Attach files to a campaign with upload_media() then media_ids=[...]; attach to a transactional email inline via attachments=[Path(...)].
- Pass a custom httpx2.Timeout via timeout_config for slow or self-hosted instances (the default is 10 seconds).
End-to-end wiring
listmonk is a flat module with global auth state, not a client object. Configure the base URL, log in once, then call functions directly. set_url_base() must come before everything, and login() returns a bool (it does not raise on bad credentials) — check it.
import listmonklistmonk.set_url_base('https://listmonk.yourdomain.com') # scheme required; no /api pathif not listmonk.login('admin', 'super-secret'): # False = rejected OR unreachableraise SystemExit('Login failed: check credentials and base URL.')# Add someone, then send them a transactional email.sub = listmonk.create_subscriber('user@example.com', 'Jane Doe', list_ids={1}, pre_confirm=True)listmonk.send_transactional_email('user@example.com', template_id=3, template_data={'name': 'Jane'})
Because auth is module-level global state, only one Listmonk instance can be targeted at a time and credential changes are not thread-safe. Every data call runs an internal state check and raises OperationNotAllowedError if the base URL is unset or you have not logged in.
Two error models: raises vs. returns False
This trips people up. Some functions raise on failure; others report failure through their return value. Do not wrap the "returns False" ones in try/except expecting an exception.
- Return `False` (never raise on failure):
login(),is_healthy(),verify_login()(rejected creds / unreachable),confirm_optin()(non-2xx status),add_subscribers_to_lists()(empty inputs or error status). - Return `None` when nothing matches:
subscriber_by_email(),subscriber_by_id(),subscriber_by_uuid(),campaign_by_id(),template_by_id(). - Raise: most create/update/delete calls raise
ValueErrorfor bad arguments,httpx2.HTTPStatusErroron 4xx/5xx, andValidationErroron an empty/malformed server body.list_by_id()returns aMailingList(notOptional) and raises if the ID is missing.
Note httpx2 (a fork of httpx with a near-identical API), not httpx: catch httpx2.HTTPStatusError and build timeouts with httpx2.Timeout(timeout=30.0).
The mutate-then-pass-back update pattern
update_subscriber, update_campaign, and update_template take the model object, not loose fields. Fetch it, mutate attributes in place, pass it back — the client sends the full record and re-fetches the server's fresh copy as the return value.
sub = listmonk.subscriber_by_email('user@example.com')sub.name = 'Updated Name'sub.attribs['rating'] = 7# List membership: existing lists - remove_from_lists + add_to_listsupdated = listmonk.update_subscriber(sub, add_to_lists={4}, remove_from_lists={5})
update_subscriber(status=...) can enable/disable/block, but for status-only changes prefer the dedicated wrappers: enable_subscriber(sub), disable_subscriber(sub), block_subscriber(sub). Use block_subscriber to unsubscribe someone while keeping their record; use delete_subscriber(email) to erase them entirely.
Subscriber querying (Listmonk SQL-ish syntax)
subscribers(query_text=...) passes a server-side SQL-like filter over the subscribers table. Custom attributes are queried through the JSONB ->> operator. This requires the subscribers:sql_query permission on the user's role or the server returns 403.
listmonk.subscribers(query_text="subscribers.email = 'user@example.com'")listmonk.subscribers(query_text="subscribers.attribs->>'city' = 'Portland'")listmonk.subscribers(list_id=3) # list filter needs no permission
Templates use Go template syntax (not Jinja)
Listmonk renders templates with Go's text/template/html/template, so the syntax is {{ ... }} with a leading dot for context — not Jinja/Django. Every template body must contain the placeholder {{ template "content" . }} exactly once, or create_template() raises ValueError before any request is sent.
# Campaign body pulls subscriber fields from .Subscriberbody = '<html><body>Hi {{ .Subscriber.FirstName }}! {{ template "content" . }}</body></html>'listmonk.create_template(name='Welcome', body=body, type='campaign')
There are two template types: 'campaign' and 'tx' (transactional). Merge data you pass to send_transactional_email(template_data=...) is available in a tx template as {{ .Tx.Data.<key> }}; subscriber fields are {{ .Subscriber.<Field> }}.
Media vs. transactional attachments — two different mechanisms
Attaching a file to a campaign is a two-step flow: upload to the media library, then reference the returned id. Attaching to a transactional email is inline via Path objects — no upload step.
from pathlib import Path# Campaign attachment: upload_media() -> media_idsmedia = listmonk.upload_media(Path('/path/to/report.png')) # or bytes + filename=listmonk.create_campaign(name='Report', subject='This month', media_ids=[media.id])# Transactional attachment: pass Paths directlylistmonk.send_transactional_email('user@example.com', template_id=3,attachments=[Path('/path/to/invoice.pdf')])
update_campaign replaces the whole attachment set each call: with media_ids=None it re-sends the campaign's existing media, media_ids=[] clears them, and a new list swaps them. It also silently drops a send_at that is already in the past so a stale schedule doesn't fail the update. The default Listmonk server only allows image extensions in the media library, and there's no delete-media endpoint in this client.
Scheduling and content types
create_campaign(send_at=datetime.now() + timedelta(hours=1)) schedules a send. content_type is 'richtext' | 'html' | 'markdown' | 'plain' for campaigns; transactional email uses 'html' | 'markdown' | 'plain' and defaults to 'markdown'. Custom email headers are a list of single-entry dicts (e.g. [{'X-Priority': '1'}]), not a single dict.
Fetching the docs as Markdown
Every page on the documentation site has a plain-Markdown twin: swap the .html extension for .md to get token-efficient source without the site chrome. For example https://mkennedy.codes/docs/listmonk/reference/subscribers.html is also available at https://mkennedy.codes/docs/listmonk/reference/subscribers.md. Prefer the .md form when reading these docs programmatically. </content>
Resources
- Full documentation
- llms.txt — Indexed API reference for LLMs
- llms-full.txt — Comprehensive documentation for LLMs
- Source code