How to Implement Transactional Email Sending via the Listmonk API

Send transactional emails through Listmonk by POSTing to /api/tx with an API key possessing the tx:send permission, specifying a template ID and recipient data, and letting the handler in cmd/tx.go render and queue the message for SMTP delivery.

Listmonk treats a transactional (TX) email as a single-message push rendered from a template and delivered immediately via your configured messenger. This guide explains the complete implementation—from preparing templates to SMTP transmission—based on the actual source code in the knadh/listmonk repository.

Prerequisites: Create a Transactional Template

Before calling the API, you need a transactional template stored in the database with type = "tx". Create this via the Listmonk UI or the POST /api/templates endpoint. The template body uses Go’s text/template syntax:

Hello {{ .Subscriber.Name }},

Your order #{{ .Data.order_id }} has been shipped.

If the template defines a subject, Listmonk uses it unless you override it in the API request. The template is cached at startup and retrieved via a.manager.GetTpl() in cmd/tx.go during request processing.

Configure API Authentication and Permissions

Listmonk authenticates transactional requests using API keys configured as “API users.”

  1. Navigate to Settings → Users and create an API user.
  2. Grant the tx:send permission (defined as the constant PermTxSend = "tx:send" in internal/auth/auth.go).
  3. Pass the key in the Authorization header using the format parsed by parseAuthHeader in internal/auth/auth.go:

Authorization: ApiKey <api_key>:<access_token>

The route POST /api/tx is registered in cmd/handlers.go with the permission middleware pm(a.SendTxMessage, "tx:send"), rejecting requests that lack this scope.

Construct the Request Payload

Listmonk accepts two content types: application/json for simple sends, and multipart/form-data when including attachments.

JSON Payload Structure

Send a POST request to /api/tx with the following fields:

{
  "template_id": 12,
  "subscriber_emails": ["alice@example.com"],
  "data": {
    "order_id": "12345"
  },
  "from_email": "no-reply@mydomain.com",
  "subject": "Your order shipped",
  "content_type": "html",
  "messenger": "email"
}

Key parameters:

  • template_id (required): Integer ID of the TX template.
  • subscriber_emails or subscriber_ids: Exactly one must be present. The handler validates this via validateTxMessage in cmd/tx.go.
  • subscriber_mode: "default", "fallback", or "external"—controls how subscriber data is resolved.
  • data: Arbitrary map accessible in templates via {{ .Data.<key> }}.
  • from_email: Overrides the global default sender.
  • subject: Overrides the template’s subject line.
  • content_type: "html" (default) or "plain".
  • headers: Optional map of additional SMTP headers (e.g., {"Reply-To": ["support@domain.com"]}).

Multipart Form Data for Attachments

To include attachments, send multipart/form-data:

  • data: A string containing the JSON payload described above.
  • file: One or more file parts. The handler reads these in cmd/tx.go, constructs models.Attachment objects using manager.MakeAttachmentHeader, and appends them to TxMessage.Attachments.

Practical Implementation Examples

cURL (JSON Only)

curl -X POST https://listmonk.example.com/api/tx \
  -H "Authorization: ApiKey $API_KEY:$TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "template_id": 12,
        "subscriber_emails": ["bob@example.com"],
        "data": {"order_id":"9876"},
        "subject": "Your order #9876 shipped",
        "from_email": "orders@example.com"
      }'

cURL with File Attachments

curl -X POST https://listmonk.example.com/api/tx \
  -H "Authorization: ApiKey $API_KEY:$TOKEN" \
  -F "data={\"template_id\":12,\"subscriber_emails\":[\"bob@example.com\"],\"data\":{\"order_id\":\"9876\"}}" \
  -F "file=@/path/to/invoice.pdf;type=application/pdf"

Python Requests

import requests
import json

API_URL = "https://listmonk.example.com/api/tx"
API_KEY = "myapikey"
TOKEN = "mytoken"

payload = {
    "template_id": 12,
    "subscriber_emails": ["carol@example.com"],
    "data": {"order_id": "1122"},
    "subject": "Your order #1122 shipped"
}

headers = {
    "Authorization": f"ApiKey {API_KEY}:{TOKEN}",
    "Content-Type": "application/json"
}

response = requests.post(API_URL, headers=headers, data=json.dumps(payload))
print(response.status_code, response.json())

Internal Architecture: From API Call to SMTP Delivery

Understanding the internal flow helps debug delivery issues:

  1. Route Handling: cmd/handlers.go registers POST /api/tx with the auth middleware requiring tx:send.
  2. Request Parsing: SendTxMessage in cmd/tx.go parses JSON or multipart input and sanitizes emails via a.importer.SanitizeEmail.
  3. Validation: validateTxMessage ensures only subscriber_emails or subscriber_ids is used, and validates subscriber modes.
  4. Template Resolution: a.manager.GetTpl(m.TemplateID) fetches the cached template.
  5. Rendering: TxMessage.Render in models/messages.go executes Go templates against the data structure {Subscriber, Tx}, producing the final body and subject.
  6. Message Construction: The handler builds a models.Message with From, To, rendered content, headers, and attachments.
  7. Messenger Push: a.manager.PushMessage(msg) routes to the appropriate messenger. For email, Emailer.Push in internal/messenger/email/email.go builds an smtppool.Email, attaches headers like Return-Path, and calls srv.pool.Send(em).

Each stage returns specific HTTP status codes—400 for validation errors, 403 for permission failures, and 200 on successful queueing.

Summary

  • Create a TX template with type = "tx" before sending.
  • Grant tx:send permission to API keys in internal/auth/auth.go.
  • POST to /api/tx with JSON for text/html or multipart for attachments.
  • Reference specific files like cmd/tx.go for the entry point and internal/messenger/email/email.go for SMTP logic.
  • Use subscriber_emails or subscriber_ids, but never both, as enforced by validateTxMessage.

Frequently Asked Questions

What is the difference between a campaign and a transactional email in Listmonk?

Campaigns are bulk messages sent to lists and managed through the campaign scheduler. Transactional emails are immediate, single-recipient sends triggered via the API, rendered from TX templates, and pushed directly to the messenger without campaign overhead.

Can I send transactional emails to addresses that are not subscribed to any list?

Yes. Set subscriber_mode to "external" in your payload. This bypasses the standard subscriber lookup, allowing you to send to any valid email address even if it does not exist in your Listmonk database.

How are attachments processed when sending via the API?

When using multipart/form-data, the handler in cmd/tx.go extracts file parts, reads the bytes, and constructs models.Attachment objects using manager.MakeAttachmentHeader. These attachments are appended to the message and encoded by the Emailer.Push method in internal/messenger/email/email.go before SMTP transmission.

Why is my transactional email request returning a 403 error?

A 403 Forbidden indicates your API key lacks the required permission. Verify that the key includes the tx:send scope, defined as the constant PermTxSend in internal/auth/auth.go. The middleware registered in cmd/handlers.go strictly enforces this permission for the /api/tx endpoint.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →