Create a template
POST/v1/templates
Create a template (logic-less Mustache).
Requires a full-access live key; test keys get 403 test_key_restricted.
Syntax: {{var}} escaped, {{{var}}} raw, {{#list}}…{{/list}} sections, {{^x}}…{{/x}} inverted sections. Exact ordering of variable_names is determined by the parser; the example order is illustrative.
Body: html and text both empty → 422 missing_required_field; template syntax errors → 422 template_render_error "<field>: <error>"; a tag that is not a variable name ({{first name}}, {{#each items}}, {{> footer}}) → 422 template_render_error "html: line L, column C: {{…}} is not a variable name: …"
variable_schema in the response is the saved list after discovery. A send is checked against the published copy, published.variable_schema.
Body parameters
namestringrequired- Limits: trimmed, 1-200 characters → 422 missing_required_field "name is required (max 200 chars)"
aliasstring- Stable handle usable instead of the id (template.id in sends, /templates/{alias}). Limits: ^[a-z0-9][a-z0-9_-]{0,62}$ → 422 invalid_parameter; duplicate → 409 conflict
subjectstring- Mustache source.
htmlstring- At least one of html and text is required. Mustache source; {{var}} is HTML-escaped, {{{var}}} raw.
textstringfromstring- Default From used when a send omits from.
variablesobject- Sample variables used by previews. Never used for sending; a variable first found in the source takes its sample as
example. variable_schemaobject[]- Declares the template’s variables (see Variable schema). Replaces the declared list when given. On every save, names the source uses that the list lacks are added (their sample value as example): a section {{#x}} as optional
any, a dotted {{x.y}} as a requiredobject, any other name as a requiredany, never stricter than sends were before, and declared names the source no longer uses are kept withunused: true. Limits: at most 200 entries, no duplicates, defaults and examples of their type → 422 invalid_parameter "variable_schema: <reason>"Show 6 fields
namestringrequired- Variable name: letters, digits,
_and-, starting with a letter or_, up to 64;globalis reserved. typestringrequired- What a send must pass. One of
string,number,boolean,url,date,list,object,any. requiredboolean- A send without a value (or with
null) and without a default is refused withtemplate_variables_missing. Defaultfalse. defaultany- Used when a send gives no value; must be of
type. exampleany- Used by previews; must be of
type. descriptionstring- For your team; at most 500 bytes.
designobject | null- The visual editor’s document (a JSON object with a numeric
version), stored with the template.nullmakes it a code template. Changinghtmlwithout sendingdesigndrops the design (it no longer describes the HTML). Limits: max 1 MB → 422 invalid_parameter "design is larger than 1 MB"; not an object with a numeric version → 422 invalid_parameter publishboolean- Publish the result in the same call. Without it the template stays a draft and is not used for sending until published. Default
false.
Errors
restricted_api_keyHTTP 403test_key_restrictedHTTP 403- test-mode keys can send test email and read, not change the account: use a live key
conflictHTTP 409- a template with this alias already exists
payload_too_largeHTTP 413- request body too large (over 4 MB)
missing_required_fieldHTTP 422invalid_parameterHTTP 422template_render_errorHTTP 422- template syntax error: "<field>: <error>"
template_render_errorHTTP 422- a tag that is not a variable name, e.g. {{first name}}, {{#each items}}, {{> footer}}: "html: line L, column C: {{…}} is not a variable name: letters, digits, _ and - (no spaces), parts joined with dots"
curl -X POST 'https://zapsmtp.xyz/v1/templates' \
-H "Authorization: Bearer $ZAP_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"name": "Welcome",
"alias": "welcome",
"subject": "Welcome, {{first_name}}",
"html": "<p>Hi {{first_name}}, your workspace is ready.</p>",
"publish": true
}'const res = await fetch('https://zapsmtp.xyz/v1/templates', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZAP_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Welcome",
"alias": "welcome",
"subject": "Welcome, {{first_name}}",
"html": "<p>Hi {{first_name}}, your workspace is ready.</p>",
"publish": true
}),
});
// 204 No Content (e.g. a delete) has no body
const data = res.status === 204 ? null : await res.json();
if (!res.ok) throw new Error(`${data.name}: ${data.message}`);
console.log(data);import os
import requests
res = requests.post(
"https://zapsmtp.xyz/v1/templates",
headers={"Authorization": f"Bearer {os.environ['ZAP_API_KEY']}"},
json={
"name": "Welcome",
"alias": "welcome",
"subject": "Welcome, {{first_name}}",
"html": "<p>Hi {{first_name}}, your workspace is ready.</p>",
"publish": True,
},
timeout=30,
)
res.raise_for_status()
print(res.json() if res.status_code != 204 else "deleted")<?php
$ch = curl_init('https://zapsmtp.xyz/v1/templates');
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ZAP_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Welcome',
'alias' => 'welcome',
'subject' => 'Welcome, {{first_name}}',
'html' => '<p>Hi {{first_name}}, your workspace is ready.</p>',
'publish' => true,
]),
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) {
throw new RuntimeException($data['message']);
}
print_r($data);package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
body := strings.NewReader(`{
"name": "Welcome",
"alias": "welcome",
"subject": "Welcome, {{first_name}}",
"html": "<p>Hi {{first_name}}, your workspace is ready.</p>",
"publish": true
}`)
req, err := http.NewRequest(
"POST",
"https://zapsmtp.xyz/v1/templates",
body,
)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("ZAP_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := io.ReadAll(res.Body)
fmt.Println(res.Status, string(out))
}require 'net/http'
require 'json'
uri = URI('https://zapsmtp.xyz/v1/templates')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{ENV.fetch('ZAP_API_KEY')}"
req['Content-Type'] = 'application/json'
req.body = {
name: "Welcome",
alias: "welcome",
subject: "Welcome, {{first_name}}",
html: "<p>Hi {{first_name}}, your workspace is ready.</p>",
publish: true
}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
puts res.code, (JSON.parse(res.body) unless res.body.to_s.empty?)import java.net.URI;
import java.net.http.*;
public class ZapRequest {
public static void main(String[] args) throws Exception {
var request = HttpRequest.newBuilder(URI.create("https://zapsmtp.xyz/v1/templates"))
.header("Authorization", "Bearer " + System.getenv("ZAP_API_KEY"))
.header("Content-Type", "application/json")
.method("POST", HttpRequest.BodyPublishers.ofString("""
{
"name": "Welcome",
"alias": "welcome",
"subject": "Welcome, {{first_name}}",
"html": "<p>Hi {{first_name}}, your workspace is ready.</p>",
"publish": true
}"""))
.build();
var response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode() + " " + response.body());
}
}using System.Net.Http.Headers;
using System.Text;
using var http = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://zapsmtp.xyz/v1/templates");
request.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("ZAP_API_KEY"));
request.Content = new StringContent("""
{
"name": "Welcome",
"alias": "welcome",
"subject": "Welcome, {{first_name}}",
"html": "<p>Hi {{first_name}}, your workspace is ready.</p>",
"publish": true
}
""", Encoding.UTF8, "application/json");
var response = await http.SendAsync(request);
Console.WriteLine($"{(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}");ZAP_API_KEY in your environment.Response 201 Created
{
"id": "019a1f23-7f80-7192-8b3c-4d5e6f7a8b9c",
"name": "Receipt",
"alias": "receipt",
"subject": "Your receipt for {{order_id}}",
"html": "<p>Hi {{name}}, thanks for order {{order_id}}.</p>",
"text": "Hi {{name}}, thanks for order {{order_id}}.",
"from": "Acme <billing@acme.com>",
"variables": {
"name": "Jane",
"order_id": "A-1042"
},
"variable_schema": [
{
"name": "order_id",
"type": "string",
"required": true,
"example": "A-1042",
"description": "Order number shown to the customer"
},
{
"name": "name",
"type": "string",
"required": false,
"default": "there",
"example": "Jane"
}
],
"design": null,
"created_at": "2026-10-21T16:10:00Z",
"updated_at": "2026-10-21T16:10:00Z",
"object": "template",
"variable_names": [
"order_id",
"name"
],
"status": "draft",
"has_unpublished_changes": false,
"published_at": null,
"published": null
}