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.”
- Navigate to Settings → Users and create an API user.
- Grant the
tx:sendpermission (defined as the constantPermTxSend = "tx:send"ininternal/auth/auth.go). - Pass the key in the
Authorizationheader using the format parsed byparseAuthHeaderininternal/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_emailsorsubscriber_ids: Exactly one must be present. The handler validates this viavalidateTxMessageincmd/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 incmd/tx.go, constructsmodels.Attachmentobjects usingmanager.MakeAttachmentHeader, and appends them toTxMessage.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:
- Route Handling:
cmd/handlers.goregistersPOST /api/txwith the auth middleware requiringtx:send. - Request Parsing:
SendTxMessageincmd/tx.goparses JSON or multipart input and sanitizes emails viaa.importer.SanitizeEmail. - Validation:
validateTxMessageensures onlysubscriber_emailsorsubscriber_idsis used, and validates subscriber modes. - Template Resolution:
a.manager.GetTpl(m.TemplateID)fetches the cached template. - Rendering:
TxMessage.Renderinmodels/messages.goexecutes Go templates against the data structure{Subscriber, Tx}, producing the final body and subject. - Message Construction: The handler builds a
models.MessagewithFrom,To, rendered content, headers, and attachments. - Messenger Push:
a.manager.PushMessage(msg)routes to the appropriate messenger. For email,Emailer.Pushininternal/messenger/email/email.gobuilds ansmtppool.Email, attaches headers likeReturn-Path, and callssrv.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:sendpermission to API keys ininternal/auth/auth.go. - POST to
/api/txwith JSON for text/html or multipart for attachments. - Reference specific files like
cmd/tx.gofor the entry point andinternal/messenger/email/email.gofor SMTP logic. - Use
subscriber_emailsorsubscriber_ids, but never both, as enforced byvalidateTxMessage.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →