Templates
Templates let you define reusable email layouts with placeholder variables. Store your HTML once, then send personalized emails by passing different data each time.
Create a template
Section titled “Create a template”POST /api/v1/templates
curl -X POST https://api.relaypost.dev/v1/templates \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html": "<h1>Thanks, {{customerName}}!</h1><p>Your order #{{orderNumber}} totaling {{total}} has been confirmed.</p>", "text": "Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed." }'const response = await fetch("https://api.relaypost.dev/v1/templates", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, body: JSON.stringify({ name: "Order Confirmation", subject: "Your order #{{orderNumber}}", html: "<h1>Thanks, {{customerName}}!</h1><p>Your order #{{orderNumber}} totaling {{total}} has been confirmed.</p>", text: "Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed.", }),});
const result = await response.json();console.log(result.data.id);import requests
response = requests.post( "https://api.relaypost.dev/v1/templates", headers={ "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, json={ "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html": "<h1>Thanks, {{customerName}}!</h1><p>Your order #{{orderNumber}} totaling {{total}} has been confirmed.</p>", "text": "Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed.", },)
result = response.json()print(result["data"]["id"])require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/templates")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Post.new(uri)request["Content-Type"] = "application/json"request["Authorization"] = "Bearer YOUR_API_KEY"request.body = {name: "Order Confirmation",subject: 'Your order #{{orderNumber}}',html: '<h1>Thanks, {{customerName}}!</h1><p>Your order #{{orderNumber}} totaling {{total}} has been confirmed.</p>',text: 'Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed.'}.to_json
response = http.request(request)data = JSON.parse(response.body)["data"]puts data["id"]use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->post("/v1/templates", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "json" => [ "name" => "Order Confirmation", "subject" => "Your order #{{orderNumber}}", "html" => "<h1>Thanks, {{customerName}}!</h1><p>Your order #{{orderNumber}} totaling {{total}} has been confirmed.</p>", "text" => "Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed.", ],]);
$data = json_decode($response->getBody(), true)["data"];echo $data["id"];Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name |
subject | string | Yes | Subject line (supports {{variables}}) |
html | string | No | HTML body (supports {{variables}}) |
text | string | No | Plain text body (supports {{variables}}) |
variable_schema | array | No | Typed variable definitions (see Variable Schema below) |
Response (201)
Section titled “Response (201)”{ "data": { "id": "tmpl_abc123", "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html_body": "<h1>Thanks, {{customerName}}!</h1><p>Your order #{{orderNumber}} totaling {{total}} has been confirmed.</p>", "text_body": "Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed.", "variables": ["orderNumber", "customerName", "total"], "variable_schema": null, "is_active": true, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" }}Send with a template
Section titled “Send with a template”Reference the template by ID and pass the variable values using POST /api/v1/emails/send:
curl -X POST https://api.relaypost.dev/v1/emails/send \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "from": { "email": "orders@yourapp.com" }, "to": [{ "email": "customer@example.com" }], "subject": "Your order #1234", "template_id": "tmpl_abc123", "template_data": { "customerName": "Jane", "orderNumber": "1234", "total": "$49.99" } }'const response = await fetch("https://api.relaypost.dev/v1/emails/send", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, body: JSON.stringify({ from: { email: "orders@yourapp.com" }, to: [{ email: "customer@example.com" }], subject: "Your order #1234", template_id: "tmpl_abc123", template_data: { customerName: "Jane", orderNumber: "1234", total: "$49.99", }, }),});
const result = await response.json();console.log(result.data.message_id);import requests
response = requests.post( "https://api.relaypost.dev/v1/emails/send", headers={ "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, json={ "from": {"email": "orders@yourapp.com"}, "to": [{"email": "customer@example.com"}], "subject": "Your order #1234", "template_id": "tmpl_abc123", "template_data": { "customerName": "Jane", "orderNumber": "1234", "total": "$49.99", }, },)
result = response.json()print(result["data"]["message_id"])require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/emails/send")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Post.new(uri)request["Content-Type"] = "application/json"request["Authorization"] = "Bearer YOUR_API_KEY"request.body = {from: { email: "orders@yourapp.com" },to: [{ email: "customer@example.com" }],subject: "Your order #1234",template_id: "tmpl_abc123",template_data: {customerName: "Jane",orderNumber: "1234",total: "$49.99"}}.to_json
response = http.request(request)data = JSON.parse(response.body)["data"]puts data["message_id"]use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->post("/v1/emails/send", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "json" => [ "from" => ["email" => "orders@yourapp.com"], "to" => [["email" => "customer@example.com"]], "subject" => "Your order #1234", "template_id" => "tmpl_abc123", "template_data" => [ "customerName" => "Jane", "orderNumber" => "1234", "total" => "$49.99", ], ],]);
$data = json_decode($response->getBody(), true)["data"];echo $data["message_id"];List templates
Section titled “List templates”GET /api/v1/templates
curl "https://api.relaypost.dev/v1/templates?page=1&limit=20" \ -H "Authorization: Bearer YOUR_API_KEY"const response = await fetch( "https://api.relaypost.dev/v1/templates?page=1&limit=20", { headers: { "Authorization": "Bearer YOUR_API_KEY" }, });
const result = await response.json();console.log(result.data);import requests
response = requests.get( "https://api.relaypost.dev/v1/templates", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"page": 1, "limit": 20},)
result = response.json()print(result["data"])require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/templates")uri.query = URI.encode_www_form(page: 1, limit: 20)http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Get.new(uri)request["Authorization"] = "Bearer YOUR_API_KEY"
response = http.request(request)result = JSON.parse(response.body)result["data"].each { |t| puts t["name"] }use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->get("/v1/templates", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "query" => ["page" => 1, "limit" => 20],]);
$result = json_decode($response->getBody(), true);foreach ($result["data"] as $t) { echo $t["name"] . "\n";}Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 20 | Results per page (max 100) |
Response (200)
Section titled “Response (200)”{ "data": [ { "id": "tmpl_abc123", "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html_body": "...", "text_body": "...", "variables": ["orderNumber", "customerName", "total"], "variable_schema": null, "is_active": true, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T10:30:00.000Z" } ], "pagination": { "page": 1, "limit": 20, "total_count": 5, "total_pages": 1 }}Update a template
Section titled “Update a template”PUT /api/v1/templates/:id
Only include the fields you want to change — everything else stays the same.
curl -X PUT https://api.relaypost.dev/v1/templates/tmpl_abc123 \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "html": "<h1>Updated layout</h1><p>Hello {{customerName}}</p>" }'const response = await fetch( "https://api.relaypost.dev/v1/templates/tmpl_abc123", { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, body: JSON.stringify({ html: "<h1>Updated layout</h1><p>Hello {{customerName}}</p>", }), });
const result = await response.json();console.log(result.data.updated_at);import requests
response = requests.put( "https://api.relaypost.dev/v1/templates/tmpl_abc123", headers={ "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY", }, json={ "html": "<h1>Updated layout</h1><p>Hello {{customerName}}</p>", },)
result = response.json()print(result["data"]["updated_at"])require "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/templates/tmpl_abc123")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Put.new(uri)request["Content-Type"] = "application/json"request["Authorization"] = "Bearer YOUR_API_KEY"request.body = {html: "<h1>Updated layout</h1><p>Hello {{customerName}}</p>"}.to_json
response = http.request(request)data = JSON.parse(response.body)["data"]puts data["updated_at"]use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->put("/v1/templates/tmpl_abc123", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"], "json" => [ "html" => "<h1>Updated layout</h1><p>Hello {{customerName}}</p>", ],]);
$data = json_decode($response->getBody(), true)["data"];echo $data["updated_at"];Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Updated template name |
subject | string | No | Updated subject line |
html | string | No | Updated HTML body |
text | string | No | Updated plain text body |
variable_schema | array | No | Updated variable definitions |
is_active | boolean | No | Activate or deactivate |
At least one field must be provided.
Response (200)
Section titled “Response (200)”{ "data": { "id": "tmpl_abc123", "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html_body": "<h1>Updated layout</h1><p>Hello {{customerName}}</p>", "text_body": "Thanks, {{customerName}}! Your order #{{orderNumber}} totaling {{total}} has been confirmed.", "variables": ["orderNumber", "customerName", "total"], "variable_schema": null, "is_active": true, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T11:00:00.000Z" }}Errors
Section titled “Errors”| Status | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | No update fields provided or invalid values |
| 404 | NOT_FOUND | Template not found or belongs to another organization |
| 409 | TEMPLATE_NAME_CONFLICT | Template name already exists |
Delete a template
Section titled “Delete a template”DELETE /api/v1/templates/:id
Soft-deletes the template by setting is_active to false. The template remains in the database so historical email records referencing it stay valid. Use PUT /api/v1/templates/:id with is_active: true to reactivate.
curl -X DELETE https://api.relaypost.dev/v1/templates/tmpl_abc123 \ -H "Authorization: Bearer YOUR_API_KEY"const response = await fetch( "https://api.relaypost.dev/v1/templates/tmpl_abc123", { method: "DELETE", headers: { "Authorization": "Bearer YOUR_API_KEY" }, });
const result = await response.json();console.log(result.data.is_active); // falseimport requests
response = requests.delete( "https://api.relaypost.dev/v1/templates/tmpl_abc123", headers={"Authorization": "Bearer YOUR_API_KEY"},)
result = response.json()print(result["data"]["is_active"]) # Falserequire "net/http"require "json"require "uri"
uri = URI("https://api.relaypost.dev/v1/templates/tmpl_abc123")http = Net::HTTP.new(uri.host, uri.port)http.use_ssl = true
request = Net::HTTP::Delete.new(uri)request["Authorization"] = "Bearer YOUR_API_KEY"
response = http.request(request)data = JSON.parse(response.body)["data"]puts "Active: #{data["is_active"]}"use GuzzleHttp\Client;
$client = new Client(["base_uri" => "https://api.relaypost.dev"]);
$response = $client->delete("/v1/templates/tmpl_abc123", [ "headers" => ["Authorization" => "Bearer YOUR_API_KEY"],]);
$data = json_decode($response->getBody(), true)["data"];echo "Active: " . ($data["is_active"] ? "true" : "false");Response (200)
Section titled “Response (200)”{ "data": { "id": "tmpl_abc123", "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html_body": "<h1>Thanks, {{customerName}}!</h1>", "text_body": "Thanks, {{customerName}}!", "variables": ["orderNumber", "customerName"], "variable_schema": null, "is_active": false, "created_at": "2025-01-15T10:30:00.000Z", "updated_at": "2025-01-15T12:00:00.000Z" }}Errors
Section titled “Errors”| Status | Code | Description |
|---|---|---|
| 404 | NOT_FOUND | Template not found or belongs to another organization |
Variable Schema
Section titled “Variable Schema”Templates support an optional variable_schema field that defines typed variable declarations. When provided, template_data is validated against the schema at send time.
Each entry in the array has the following shape:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Variable name as referenced in template content |
type | string | Yes | string, number, boolean, array, or object |
required | boolean | Yes | Whether the variable must be provided at send time |
default_value | any | No | Default value when the variable is not provided (must match type) |
Example: Creating a template with a schema
Section titled “Example: Creating a template with a schema”{ "name": "Order Confirmation", "subject": "Your order #{{orderNumber}}", "html": "<h1>Thanks, {{customerName}}!</h1><p>Total: {{total}}</p>", "variable_schema": [ { "name": "customerName", "type": "string", "required": true }, { "name": "orderNumber", "type": "string", "required": true }, { "name": "total", "type": "string", "required": true }, { "name": "items", "type": "array", "required": false, "default_value": [] } ]}Auto-detected variables vs user-defined schema
Section titled “Auto-detected variables vs user-defined schema”Variables are automatically detected from {{variable}} placeholders in the subject, HTML, and text bodies. The variables field in the response always lists all detected variables, regardless of whether a schema is defined.
When variable_schema is provided:
- Required variables that are missing at send time return a
TEMPLATE_MISSING_VARIABLESerror - Variables with the wrong type return a
TEMPLATE_DATA_INVALIDerror - Variables with
required: falseand adefault_valueuse the default when not provided
When variable_schema is not provided:
- No validation is performed on
template_data - Missing variables render as empty strings
Undeclared variable warnings
Section titled “Undeclared variable warnings”If a {{variable}} appears in the template content but is not declared in variable_schema, the API returns a warning in the response when creating or updating the template:
{ "data": { "...": "..." }, "warnings": ["Undeclared variable: discount"]}This is informational only — the template is still created/updated successfully.
Preview a template
Section titled “Preview a template”POST /api/v1/templates/:id/preview
Preview a rendered template with sample data before sending. Missing variables render as empty strings and are listed in the warnings array. Inactive templates can be previewed.
See the REST API Reference for full request/response details and code examples in all languages.
Batch send with templates
Section titled “Batch send with templates”POST /api/v1/emails/batch
Send the same template to multiple recipients with per-recipient variable data. Maximum 1000 recipients per batch.
See the REST API Reference for full request/response details and code examples in all languages.