Templates
Create and manage reusable email templates with dynamic variable substitution
Create reusable email templates with dynamic variables. Templates store your email subject, HTML, and text content so you can send personalized emails without rebuilding content on every request.
Create a Template
POST https://api.mail.land/v1/templatescurl -X POST https://api.mail.land/v1/templates \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}} is confirmed",
"htmlContent": "<h1>Thanks, {{firstName}}!</h1><p>Order #{{orderNumber}} has been confirmed. Total: {{total}}</p>",
"textContent": "Thanks, {{firstName}}! Order #{{orderNumber}} has been confirmed. Total: {{total}}",
"categoryId": "k3v9q2m7xw4tpa"
}'{
"id": "t8x2m4q9zk7wpr",
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}} is confirmed",
"htmlContent": "<h1>Thanks, {{firstName}}!</h1>...",
"textContent": "Thanks, {{firstName}}!...",
"categoryId": "k3v9q2m7xw4tpa",
"categoryName": "Transactional",
"categoryColor": "#3B82F6",
"createdAt": "2026-02-17T10:00:00Z",
"updatedAt": null
}Validation Rules
| Field | Constraint |
|---|---|
name | Required, max 200 characters |
subject | Required, max 500 characters |
htmlContent | Required, max 1 MB |
textContent | Optional, max 1 MB |
categoryId | Optional, must reference an existing category |
Variable Syntax
Use double curly braces for variable substitution:
<h1>Hello {{firstName}},</h1>
<p>Your order #{{orderNumber}} shipped on {{shipDate}}.</p>
<p>Track your package: <a href="{{trackingUrl}}">{{trackingNumber}}</a></p>Variables are replaced at send time. Pass values via the variables field in your send request:
{
"from": { "email": "[email protected]" },
"to": [
{
"email": "[email protected]",
"variables": {
"firstName": "Jane",
"orderNumber": "ORD-12345",
"shipDate": "Feb 17, 2026",
"trackingUrl": "https://track.example.com/ABC123",
"trackingNumber": "ABC123"
}
}
],
"templateId": "t8x2m4q9zk7wpr"
}Variables work in both the subject and htmlContent/textContent. Every {{variable}} a template uses must have a value, either in the request's variables or in the recipient's own variables. If one has no value, MailLand does not send the message with the placeholder in it: POST /v1/transactional/send answers 422 TEMPLATE_VARIABLE_MISSING and names the missing variables, and POST /v1/transactional/send-batch skips that recipient and lists it in rejectedRecipients. Campaigns and automations skip the recipient too. Inline htmlContent sent without any variables is sent as written. Placeholders are exactly {{name}}, with no filters, conditions or loops. A template reports grammarVersion: "legacy-v1", the only grammar today; you can omit the field when you create or update a template.
Default Values
A template can carry a default value for each contact field it uses, so a subscriber who has no value for that field still receives the email. Set them with variableDefaults, an object of placeholder name to value, on create and update. It is returned when you get, list or duplicate a template.
{
"name": "Welcome",
"subject": "Hello {{firstName}}",
"htmlContent": "<p>Hi {{firstName}}</p>",
"variableDefaults": { "firstName": "there" }
}Rules:
- Defaults apply to campaign and automation sends only. A recipient's own non-blank value always wins over the default.
- A field with neither a value nor a default is still refused: MailLand never sends a literal
{{firstName}}. - Transactional API and SMTP sends are not affected. There, the values come from your request, and a missing one is refused as described above.
- On update, leaving
variableDefaultsout keeps the stored defaults. Send{}to clear them. - Names follow the placeholder syntax.
emailandsubscriberIdare always supplied and cannot have a default. A value is one line of at most 200 characters, and a template holds at most 50 defaults.
In the dashboard editor, each placeholder has a default input that shows how many subscribers are missing that field, and saving warns you if a field with missing subscribers has no default.
Preview a Template
Preview a template with sample values auto-filled for detected variables:
GET https://api.mail.land/v1/templates/{id}/previewcurl https://api.mail.land/v1/templates/t8x2m4q9zk7wpr/preview \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"id": "t8x2m4q9zk7wpr",
"name": "Order Confirmation",
"subject": "Your order #12345 is confirmed",
"renderedHtml": "<h1>Thanks, John!</h1><p>Order #12345 has been confirmed.</p>",
"renderedText": "Thanks, John! Order #12345 has been confirmed."
}Duplicate a Template
Clone an existing template:
POST https://api.mail.land/v1/templates/{id}/duplicatecurl -X POST https://api.mail.land/v1/templates/t8x2m4q9zk7wpr/duplicate \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."The duplicate receives an auto-generated name: "Order Confirmation (Copy)". If a copy already exists, the name increments: "Order Confirmation (Copy 2)", etc.
Template Categories
Organize templates into categories with optional colors.
Create a category
POST https://api.mail.land/v1/templates/categoriescurl -X POST https://api.mail.land/v1/templates/categories \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Transactional",
"description": "Order confirmations, receipts, and shipping notifications",
"color": "#3B82F6"
}'| Field | Constraint |
|---|---|
name | Required, 1-100 characters |
description | Optional, max 500 characters |
color | Optional, hex format (#3B82F6 or #38F) |
List categories
GET https://api.mail.land/v1/templates/categoriescurl https://api.mail.land/v1/templates/categories \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"categories": [
{
"id": "k3v9q2m7xw4tpa",
"name": "Transactional",
"description": "Order confirmations, receipts",
"color": "#3B82F6",
"templateCount": 12,
"createdAt": "2026-01-15T10:00:00Z"
}
],
"totalCount": 3
}List Templates
GET https://api.mail.land/v1/templates?limit=50curl "https://api.mail.land/v1/templates?limit=50" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Filter by category:
GET https://api.mail.land/v1/templates?categoryId=k3v9q2m7xw4tpacurl "https://api.mail.land/v1/templates?categoryId=k3v9q2m7xw4tpa" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"items": [
{
"id": "t8x2m4q9zk7wpr",
"name": "Order Confirmation",
"subject": "Your order #{{orderNumber}} is confirmed",
"htmlContent": "...",
"categoryName": "Transactional",
"categoryColor": "#3B82F6",
"createdAt": "2026-02-17T10:00:00Z"
}
],
"nextCursor": "eyJ2IjoxLCJzIjoiY3JlYXRlZGF0OmRlc2MiLCJrIjoi...",
"hasMore": true
}Update a Template
PUT https://api.mail.land/v1/templates/{id}All fields are required in the update request (this is a full replacement, not a partial update):
curl -X PUT https://api.mail.land/v1/templates/t8x2m4q9zk7wpr \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Order Confirmation v2",
"subject": "Order #{{orderNumber}} confirmed - {{companyName}}",
"htmlContent": "<h1>Thanks, {{firstName}}!</h1><p>Updated template content.</p>",
"textContent": "Thanks, {{firstName}}! Updated template content.",
"categoryId": "k3v9q2m7xw4tpa"
}'Delete a Template
DELETE https://api.mail.land/v1/templates/{id}curl -X DELETE https://api.mail.land/v1/templates/t8x2m4q9zk7wpr \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Returns 204 No Content on success. A deleted template can no longer be retrieved, listed, or used to send: a send request referencing it, immediate or scheduled, is refused with 400 TEMPLATE_NOT_FOUND, and a schedule whose template is deleted before its due time is refused the same way at send time.
Using Templates When Sending
Reference a template by ID in your send request. The template's subject and content are used, with variables substituted:
{
"from": { "email": "[email protected]" },
"to": [
{
"email": "[email protected]",
"variables": {
"firstName": "Jane",
"orderNumber": "ORD-12345"
}
}
],
"templateId": "t8x2m4q9zk7wpr",
"variables": {
"companyName": "Acme Inc"
}
}When templateId is set, the template's rendered subject is always used; a subject in the send request is ignored.
Plan Limits
The number of templates you can create depends on your plan. If you reach the limit, creating a new template returns 403 PLAN_LIMIT_REACHED.
Next Steps
- REST API: send emails with templates
- Batch Sending: use templates with batch emails
- Scheduled Emails: use templates via
/v1/transactional/sendwithscheduledAt(the scheduled-emails endpoint has notemplateId)