Complete Python SDK, CLI, and automation workflows for Acumbamail email marketing.
Table of Contents
- Features
- Installation
- Getting Started
- Usage
- API Reference
- What the API doesn't show you
- Error Handling
- Claude Code Skill
- n8n Integration
- Examples
- Contributing
- License
acumbamail is a Python SDK and CLI with complete coverage of the Acumbamail REST API. It provides sync and async clients, a JSON-outputting CLI for shell scripting and CI pipelines, and — uniquely — automation workflows as YAML code so you can version-control and deploy email sequences the same way you deploy software.
>>> from acumbamail import AcumbamailClient
>>> client = AcumbamailClient(auth_token="...", default_sender_email="hello@company.com")
>>> lst = client.create_list("Newsletter")
>>> client.add_subscriber("user@example.com", lst.id, fields={"name": "Alice"})
>>> campaign = client.create_campaign(
... name="Welcome", subject="Thanks for subscribing!",
... content='<p>Hello!</p><a href="*|UNSUBSCRIBE_URL|*">Unsubscribe</a>',
... list_ids=[lst.id],
... )
>>> stats = client.get_campaign_total_information(campaign.id)
>>> f"{stats.opened / stats.total_delivered:.1%} open rate"
'28.3% open rate'
Features
- Sync and async clients —
AcumbamailClientandAsyncAcumbamailClientshare the same API surface; async client is an async context manager. - Full API coverage — 48 endpoints across lists, subscribers, campaigns, templates, SMTP, and webhooks.
- JSON CLI — every command outputs JSON, piped straight into
jqor CI scripts. - Automations as code — define email sequences in YAML, deploy with
acumbamail automations deploy. Version-controlled, idempotent, reviewable. - Automatic retry — 429 responses are retried up to 3× with exponential backoff; no extra configuration needed.
- Typed models — every response is a typed dataclass (
MailList,Campaign,CampaignTotalInformation, …). - OpenAPI 3.0.3 spec and Postman collection included in the repository.
- Claude Code skill — install a bundled skill so AI assistants understand the full CLI and SDK.
Installation
# Using pip
pip install acumbamail
# Using uv
uv add acumbamail
# CLI only (no Python import needed)
pipx install acumbamail
Requires Python 3.13+.
Getting Started
1. Get your API token — log in to acumbamail.com, go to Settings → API, and copy your token.
2. Set the environment variable:
export ACUMBAMAIL_TOKEN=your-token-here
3. Create a list and send your first campaign:
from acumbamail import AcumbamailClient
client = AcumbamailClient(
auth_token="your-token",
default_sender_email="hello@mycompany.com",
default_sender_name="My Company",
)
# Create a mailing list
lst = client.create_list("My Newsletter")
print(f"List created: {lst.id}")
# Add a subscriber
client.add_subscriber("alice@example.com", lst.id, fields={"nombre": "Alice"})
# Send your first campaign
campaign = client.create_campaign(
name="First Campaign",
subject="Hello from acumbamail!",
content="""
<h1>Hello *|FNAME|*!</h1>
<p>Welcome to our newsletter.</p>
<a href="*|UNSUBSCRIBE_URL|*">Unsubscribe</a>
""",
list_ids=[lst.id],
)
print(f"Campaign sent: {campaign.id}")
# Check results the next day
stats = client.get_campaign_total_information(campaign.id)
print(f"Delivered: {stats.total_delivered}, Opened: {stats.opened}")
Or with the CLI:
$ acumbamail lists create --name "My Newsletter" --sender-email hello@mycompany.com
{"id": 1138335, "name": "My Newsletter"}
$ acumbamail subscribers add --list-id 1138335 --email alice@example.com
{"email": "alice@example.com", "list_id": 1138335}
$ acumbamail campaigns create \
--name "First Campaign" --subject "Hello!" \
--html '<h1>Hi!</h1><a href="*|UNSUBSCRIBE_URL|*">Unsubscribe</a>' \
--list-id 1138335 --from-email hello@mycompany.com
{"id": 3294239, "name": "First Campaign", ...}
[!TIP] Your sender email must be verified in Acumbamail before sending campaigns. Add it at Settings → Verified senders.
Usage
Python SDK
from acumbamail import AcumbamailClient
client = AcumbamailClient(
auth_token="your-token",
default_sender_name="My Company",
default_sender_email="hello@mycompany.com",
)
# Lists and subscribers
lists = client.get_lists()
client.add_subscriber("user@example.com", list_id=lists[0].id)
client.batch_add_subscribers(lists[0].id, [
{"email": "a@example.com", "nombre": "Alice"},
{"email": "b@example.com", "nombre": "Bob"},
])
# Campaigns
campaign = client.create_campaign(
name="May Newsletter",
subject="What's new this month",
content='<h1>Hello *|FNAME|*</h1><a href="*|UNSUBSCRIBE_URL|*">Unsubscribe</a>',
list_ids=[lists[0].id],
)
# Analytics
stats = client.get_campaign_total_information(campaign.id)
clicks = client.get_campaign_clicks(campaign.id)
openers = client.get_campaign_openers_by_browser(campaign.id)
Or use async with AsyncAcumbamailClient…
import asyncio
from acumbamail import AsyncAcumbamailClient
async def main():
async with AsyncAcumbamailClient(auth_token="your-token") as client:
lists = await client.get_lists()
stats = await client.get_campaign_total_information(campaign_id=123)
print(f"Open rate: {stats.opened / stats.total_delivered:.1%}")
asyncio.run(main())
The async client is identical in API to the sync client. Use async with or call .close() manually.
CLI
All commands output JSON. Pipe into jq for filtering.
$ export ACUMBAMAIL_TOKEN=your-token
$ acumbamail lists list
[{"id": 1138335, "name": "Newsletter", "subscribers_count": 1200, ...}]
$ acumbamail subscribers batch-add --list-id 1138335 --file subscribers.json
[{"email": "a@x.com", "subscriber_id": 111}, ...]
$ acumbamail campaigns create \
--name "May Newsletter" \
--subject "What's new this month" \
--html-file email.html \
--list-id 1138335 \
--from-email hello@mycompany.com
{"id": 3294239, "name": "May Newsletter", "subject": "What's new this month", ...}
$ acumbamail campaigns stats --campaign-id 3294239 | \
jq '{open_rate: (.opened / .total_delivered * 100 | round | tostring + "%")}'
{"open_rate": "28%"}
All available commands…
$ acumbamail lists list
$ acumbamail lists create --name "My List" --sender-email hello@mycompany.com
$ acumbamail lists delete --list-id 123
$ acumbamail lists stats --list-id 123
$ acumbamail subscribers list --list-id 123
$ acumbamail subscribers list --status inactive # unsubscribes + bounces, account-wide
$ acumbamail subscribers check --list-id 123 --email user@example.com
$ acumbamail subscribers add --list-id 123 --email user@example.com
$ acumbamail subscribers delete --list-id 123 --email user@example.com
$ acumbamail subscribers search --query user@example.com
$ acumbamail subscribers unsubscribe --list-id 123 --email user@example.com
$ acumbamail subscribers batch-add --list-id 123 --file subscribers.json
$ acumbamail campaigns list
$ acumbamail campaigns create --name "..." --subject "..." --html-file email.html --list-id 123
$ acumbamail campaigns info --campaign-id 456
$ acumbamail campaigns stats --campaign-id 456
$ acumbamail webhooks smtp-get
$ acumbamail webhooks smtp-config --url https://myapp.com/hooks/smtp --delivered --active
$ acumbamail webhooks list-get --list-id 123
$ acumbamail webhooks list-config --list-id 123 --url https://myapp.com/hooks/list --subscribes --active
$ acumbamail automations login
$ acumbamail automations list
$ acumbamail automations deploy workflow.yaml
$ acumbamail automations export --name "welcome-sequence"
$ acumbamail automations delete --name "welcome-sequence"
Use --token <token> as an alternative to ACUMBAMAIL_TOKEN. Every command accepts --help.
Automations as Code
Most email platforms treat automations as a GUI-only feature: you click through a visual editor, and the result lives exclusively in their cloud — invisible to Git, impossible to review in a PR, painful to replicate across accounts.
acumbamail takes a different approach. Your automation workflows live as YAML files in your repository, right next to your code. You deploy them with a single command, the same way you'd apply a Terraform plan or a Kubernetes manifest.
What you get:
- Version control — every change to a workflow goes through a commit.
git diffshows exactly what changed between the old and new sequence. - Code review — workflows are PR-reviewable. Your team can catch logic errors before a broken sequence goes out to thousands of subscribers.
- Reproducibility — recreate any automation on a new account or staging environment in seconds, from a file.
- Idempotency — deploying the same file twice is always safe. Acumbamail identifies workflows by
name, replaces in place, and reports"action": "updated". - Backup and recovery —
acumbamail automations exportpulls any existing workflow back to YAML. Even automations built in the GUI can be exported, put under version control, and managed from that point on.
# welcome-sequence.yaml ← lives in your repo, reviewed in PRs
name: welcome-sequence
trigger:
list_id: 1138335
event: subscriber_added # fires when someone joins the list
steps:
- type: email_template
subject: "Welcome aboard!"
template_id: 8986619
- type: delay
wait: 3
unit: days
- type: email_template
subject: "Getting started guide"
template_id: 8986620
- type: delay
wait: 7
unit: days
# Branch on engagement: reward active subscribers, re-engage quiet ones
- type: condition
on_match:
- type: email_template
subject: "Special offer for active subscribers"
template_id: 8986621
on_no_match:
- type: email_template
subject: "We miss you — come back?"
template_id: 8986622
$ acumbamail automations login # one-time: opens Chrome, saves session for 30 days
$ acumbamail automations deploy welcome-sequence.yaml
{"workflow_id": 36215, "action": "created", "active": false}
$ # Edit the YAML, redeploy — safe and idempotent
$ acumbamail automations deploy welcome-sequence.yaml
{"workflow_id": 36215, "action": "updated", "active": false}
$ # Pull an existing automation back to YAML (even ones built in the GUI)
$ acumbamail automations export --name "welcome-sequence" > welcome-sequence.yaml
$ acumbamail automations list | jq '[.[] | select(.active == true)]'
[!NOTE] Automations use Acumbamail's web session (not the API token). Run
acumbamail automations loginonce — the session lasts 30 days.
Supported step types: email_template, delay, condition, until, webhook, update_field, move_to.
API Reference
Mailing Lists
| Method | Description |
|---|---|
get_lists() |
All lists with subscriber counts |
create_list(name, description) |
Create a list |
delete_list(list_id) |
Delete a list |
get_list_stats(list_id) |
Subscriber, bounce, and unsubscribe counts |
get_fields(list_id) |
{field_name: field_type} mapping |
add_merge_tag(list_id, field_name, field_type) |
Add a custom field |
Subscribers
| Method | Description |
|---|---|
get_subscribers(list_id, ...) |
Paginated subscriber list — active only |
add_subscriber(email, list_id, fields, double_optin, update_subscriber) |
Add one subscriber |
delete_subscriber(email, list_id) |
Permanently remove |
unsubscribe_subscriber(list_id, email) |
Mark as unsubscribed without deleting |
batch_add_subscribers(list_id, subscribers_data, update_subscriber) |
Bulk add |
delete_all_subscribers(list_id) |
Remove all (background process) |
get_subscriber_details(list_id, subscriber_email) |
Full subscriber record |
search_subscriber(query) |
Search across all lists — active and unsubscribed only |
get_inactive_subscribers(date_from, date_to, full_info) |
Unsubscribes and bounces in a date range — account-wide, no list filter |
Before you conclude "this email isn't in the list", read What the API doesn't show you. Three of these methods return incomplete answers by design, and none of them tell you so.
Campaigns
| Method | Description |
|---|---|
create_campaign(name, subject, content, list_ids, scheduled_at, ...) |
Create and send (or schedule) |
send_template_campaign(name, subject, template_id, list_ids, ...) |
Send from a saved template |
get_campaigns() |
List all campaigns |
get_campaign_basic_information(campaign_id) |
Name, subject, status |
get_campaign_total_information(campaign_id) |
Full stats — CampaignTotalInformation |
get_campaign_clicks(campaign_id) |
Per-URL click data |
get_campaign_openers(campaign_id) |
Per-subscriber open data |
get_campaign_openers_by_browser(campaign_id) |
Opens grouped by browser |
get_campaign_openers_by_os(campaign_id) |
Opens grouped by OS |
get_campaign_openers_by_countries(campaign_id) |
Opens grouped by country |
get_campaign_soft_bounces(campaign_id) |
Soft bounce records |
get_stats_by_date(list_id, date_from, date_to) |
Aggregate stats for a list over a date range |
[!IMPORTANT] Campaign HTML must include
*|UNSUBSCRIBE_URL|*— the API rejects content without it. Merge tags*|FNAME|*,*|LNAME|*,*|EMAIL|*, and*|FULLNAME|*are also available.
Templates
| Method | Description |
|---|---|
get_templates() |
All templates |
create_template(template_name, html_content, subject) |
Create a template |
duplicate_template(template_name, origin_template_id) |
Clone a template |
SMTP Transactional
[!NOTE] Requires SMTP plan activation in your Acumbamail account.
| Method | Description |
|---|---|
send_single_email(to_email, subject, content, ...) |
One transactional email |
send_emails(messages) |
Batch transactional emails |
send_certified_email(to_email, subject, content, ...) |
Certified email |
get_email_status(email_key) |
Delivery status |
get_smtp_credits() |
Remaining credits |
Webhooks
| Method | Description |
|---|---|
get_smtp_webhook() |
Current SMTP webhook config |
config_smtp_webhook(callback_url, ...) |
Set SMTP webhook |
get_list_webhook(list_id) |
Current list webhook config |
config_list_webhook(list_id, callback_url, ...) |
Set list webhook |
What the API doesn't show you
Three silences in the Acumbamail API that cost real money if you trust the obvious reading. All three were found the hard way, auditing a list before a send.
A subscriber list has more people in it than any endpoint will list. get_subscribers() returns active subscribers only. lists list reports a subscribers_count that also includes unsubscribes and bounces. Compare the two numbers and it looks like subscribers went missing. They didn't: they're inactive, and inactive people are only reachable through a different endpoint.
That different endpoint is account-wide and lies about it. getInactiveSubscribers accepts a list_id, returns HTTP 200, and ignores it. Pass a list ID that doesn't exist and it still hands back every inactive address in the account. The records come back with a null list ID, so filtering afterwards isn't possible either. This SDK refuses list_id outright rather than let you believe you filtered:
client.get_inactive_subscribers(list_id=123, date_from=..., date_to=...)
# AcumbamailValidationError: get_inactive_subscribers() is global to the account...
And some subscribers are invisible to every listing there is. Anyone who signed up through a form but never confirmed the double opt-in sits at status='not verified' and appears in nothing: not get_subscribers, not search_subscriber, not get_inactive_subscribers, not the CSV exports. The only way to see one is to ask about that exact address:
$ acumbamail subscribers check --list-id 123 --email someone@example.com
[{"email": "someone@example.com", "found": true, "status": "not verified", "id": 5774498602, "fields": null}]
Run that before importing a list of addresses you believe are new. "Doesn't appear" is not the same as "isn't there", and adding someone who is already in the list at another status is how people end up emailing addresses they meant to leave alone.
One more thing worth knowing if you read reason codes: 1 is a bounce and 3 is a voluntary unsubscribe. Acumbamail's own documentation had it backwards. Verified by cross-checking 805 inactive records against the CSV exports for the same list. Use reason_label and skip the integers.
Error Handling
from acumbamail import (
AcumbamailError, # base class
AcumbamailValidationError, # bad arguments — 400 (e.g. missing UNSUBSCRIBE_URL)
AcumbamailAPIError, # HTTP errors — 4xx/5xx
AcumbamailRateLimitError, # 429 — retried automatically up to 3×
)
try:
client.create_campaign(
name="Test", subject="Hi",
content="<p>No unsubscribe link here</p>", # will raise
list_ids=[123],
)
except AcumbamailValidationError as e:
print(f"Validation error: {e}")
except AcumbamailRateLimitError:
print("Rate limited — the SDK already retried 3× with backoff")
except AcumbamailAPIError as e:
print(f"API error: {e}")
Claude Code Skill
If you use Claude Code, install the bundled skill so your AI assistant knows the complete CLI API and quirks:
$ acumbamail install-skills # project-local → .claude/skills/
$ acumbamail install-skills -g # user-global → ~/.claude/skills/
Two skills are installed: acumbamail-cli (lists, subscribers, campaigns, webhooks, templates) and acumbamail-automations (YAML schema, deploy/export/delete workflows).
n8n Integration
If you use n8n for workflow automation, the n8n-nodes-acumbamail community node brings the full Acumbamail API into your visual workflows — no code required.
46 operations across 6 resources — lists, subscribers, campaigns, templates, SMTP, and webhooks — plus a trigger node for real-time webhook events (new subscriptions, bounces, clicks, opens).
| Resource | Operations |
|---|---|
| List | Get All, Create, Delete, Get Stats, Get Fields, Get Merge Fields, Add Merge Tag, Get Forms, … |
| Subscriber | Get All, Add, Delete, Unsubscribe, Batch Add, Get Details, Search, Get Inactive, … |
| Campaign | Get All, Create, Get Stats, Get Clicks, Get Openers by Browser / OS / Country, … |
| Template | Get All, Create, Duplicate, Send Campaign |
| SMTP | Send Email, Send Batch, Send Certified, Get Status, Get Credits |
| Webhook (trigger) | New subscription, unsubscription, bounce, spam complaint, open, click, delivered |
Install in n8n:
Settings → Community Nodes → n8n-nodes-acumbamail
→ github.com/cr0hn/n8n-nodes-acumbamail
Examples
Runnable examples are in examples/:
| File | What it shows |
|---|---|
sync_example.py |
Basic synchronous workflow |
async_example.py |
Async workflow with AsyncAcumbamailClient |
campaign_analytics.py |
Open/click rates, browser and OS breakdown |
bulk_operations.py |
batch_add_subscribers, inactive subscriber cleanup |
ab_testing.py |
Two-variant campaign comparison |
automated_workflows.py |
List creation, bulk import, campaign, webhook setup |
error_handling.py |
Catching and handling every exception type |
$ export ACUMBAMAIL_TOKEN=your-token
$ python examples/sync_example.py
Contributing
Contributions are welcome. Please open an issue before starting significant work.
git clone https://github.com/cr0hn/python-acumbamail
cd python-acumbamail
uv sync
uv run pytest # unit + structural tests (no network)
uv run pytest -m contract # contract tests against the real API (requires ACUMBAMAIL_TOKEN)
See CONTRIBUTING.md for the full guide.
License
MIT © Daniel Garcia (cr0hn)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file acumbamail-0.4.0.tar.gz.
File metadata
- Download URL: acumbamail-0.4.0.tar.gz
- Upload date:
- Size: 200.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87f82cea4bc1ff18fa9aea0f867f4688d7163d202ae93acb3ff1b368e3b1264e
|
|
| MD5 |
586f0af68200a33acd7d44c8d776edf9
|
|
| BLAKE2b-256 |
512723e06a28ab37295fd6109dac3c7f77b917edb8039fdadc56e86f74980d25
|
Provenance
The following attestation bundles were made for acumbamail-0.4.0.tar.gz:
Publisher:
publish-to-pypi.yaml on cr0hn/python-acumbamail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
acumbamail-0.4.0.tar.gz -
Subject digest:
87f82cea4bc1ff18fa9aea0f867f4688d7163d202ae93acb3ff1b368e3b1264e - Sigstore transparency entry: 2334954335
- Sigstore integration time:
-
Permalink:
cr0hn/python-acumbamail@c34c265cc4660ff9ed887767777cc8a764d04513 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/cr0hn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yaml@c34c265cc4660ff9ed887767777cc8a764d04513 -
Trigger Event:
push
-
Statement type:
File details
Details for the file acumbamail-0.4.0-py3-none-any.whl.
File metadata
- Download URL: acumbamail-0.4.0-py3-none-any.whl
- Upload date:
- Size: 74.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6b25a2b54f9fc16024671e56b96ba4c6086f5b76871639702d71f8a44291e645
|
|
| MD5 |
565d2c8c316565f998a8c1a1676ff083
|
|
| BLAKE2b-256 |
7c454b337ade21a83dc6327bb76eaeacfef1dea362c6ffbe3a1c0062ff32be8d
|
Provenance
The following attestation bundles were made for acumbamail-0.4.0-py3-none-any.whl:
Publisher:
publish-to-pypi.yaml on cr0hn/python-acumbamail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
acumbamail-0.4.0-py3-none-any.whl -
Subject digest:
6b25a2b54f9fc16024671e56b96ba4c6086f5b76871639702d71f8a44291e645 - Sigstore transparency entry: 2334954357
- Sigstore integration time:
-
Permalink:
cr0hn/python-acumbamail@c34c265cc4660ff9ed887767777cc8a764d04513 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/cr0hn
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-to-pypi.yaml@c34c265cc4660ff9ed887767777cc8a764d04513 -
Trigger Event:
push
-
Statement type: