Customize storefront templates and preview changes
/admin/api/templates
List all storefront template names with their override status for the active theme.
/admin/api/templates/{name}
Get the content of a template. Returns the custom override if one exists, otherwise the built-in default.
/admin/api/templates/{name}/default
Get the built-in default content for a template, ignoring any custom override.
/admin/api/templates/{name}
Create or update a custom template override. Validates Tera syntax before saving. Invalidates the template cache for the active theme.
/admin/api/templates/{name}
Delete a custom template override, reverting to the built-in default. Returns 404 if no custom override exists.
/admin/api/templates/{name}/preview
Render a template with sample data and return the HTML preview. Validates Tera syntax and reports render errors.
/admin/api/email-templates
List all email template names with their override status for the active theme.
/admin/api/email-templates/{name}
Get the content of an email template. Returns the custom override if one exists, otherwise the built-in default.
/admin/api/email-templates/{name}/default
Get the built-in default content for an email template, ignoring any custom override.
/admin/api/email-templates/{name}
Create or update a custom email template override. Validates Tera syntax before saving. Email templates are standalone HTML and do not extend the storefront base layout.
/admin/api/email-templates/{name}
Delete a custom email template override, reverting to the built-in default. Returns 404 if no custom override exists.
/admin/api/email-templates/{name}/preview
Render an email template with sample order data and return the HTML preview. Sample context includes shop_name, order_id, customer_name, customer_email, items, subtotal, tax_total, total, and shipping address fields.
/admin/api/email-templates/{name}/send-preview
Send a real preview email rendered from the supplied draft (or, when `content` is omitted, the active theme's saved override / built-in default). The body uses the same per-template sample context as the in-browser preview, so what the admin sees on screen matches what lands in the inbox. The subject is the production subject prefixed with `[TEST]`.
/admin/api/template-variants
List every distinct template-variant slug of the requested kind across all themes, annotated with which themes define each slug and whether the currently active theme is one of them. Used to populate the Template dropdown on product and category edit forms. Results are sorted by slug ascending; the themes array within each entry is sorted by name ascending.
/admin/api/templates
List all storefront template names with their override status for the active theme.
[
{
"name": "base",
"has_custom": false,
"updated_at": null
},
{
"name": "home",
"has_custom": true,
"updated_at": "2025-12-01T10:00:00Z"
},
{
"name": "product",
"has_custom": false,
"updated_at": null
},
{
"name": "browse",
"has_custom": false,
"updated_at": null
},
{
"name": "cart",
"has_custom": false,
"updated_at": null
},
{
"name": "checkout",
"has_custom": false,
"updated_at": null
}
]
curl "https://yourshop.zeroshop.io/admin/api/templates" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/templates",
headers={"Authorization": "Bearer zspat_..."}
)
templates = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/templates", {
headers: { "Authorization": "Bearer zspat_..." }
});
const templates = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/templates")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
templates = JSON.parse(resp.body)
/admin/api/templates/{name}
Get the content of a template. Returns the custom override if one exists, otherwise the built-in default.
{
"name": "home",
"content": "<h1>{{ t.home_hero_title | default(value='Welcome') }}</h1>\n<p>{{ t.home_hero_subtitle | default(value='Browse our products') }}</p>",
"is_custom": true
}
curl "https://yourshop.zeroshop.io/admin/api/templates/home" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/templates/home",
headers={"Authorization": "Bearer zspat_..."}
)
template = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/templates/home", {
headers: { "Authorization": "Bearer zspat_..." }
});
const template = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/templates/home")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
template = JSON.parse(resp.body)
/admin/api/templates/{name}/default
Get the built-in default content for a template, ignoring any custom override.
{
"name": "home",
"content": "{% extends \"base\" %}...",
"is_custom": false
}
curl "https://yourshop.zeroshop.io/admin/api/templates/home/default" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/templates/home/default",
headers={"Authorization": "Bearer zspat_..."}
)
template = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/templates/home/default", {
headers: { "Authorization": "Bearer zspat_..." }
});
const template = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/templates/home/default")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
template = JSON.parse(resp.body)
/admin/api/templates/{name}
Create or update a custom template override. Validates Tera syntax before saving. Invalidates the template cache for the active theme.
{
"content": "<h1>{{ t.home_hero_title | default(value='Welcome to our shop') }}</h1>"
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/templates/home" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"content": "<h1>{{ t.home_hero_title | default(value='"'"'Welcome'"'"') }}</h1>"}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/templates/home",
headers={"Authorization": "Bearer zspat_..."},
json={"content": "<h1>{{ t.home_hero_title | default(value='Welcome') }}</h1>"}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/templates/home", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
content: "<h1>{{ t.home_hero_title | default(value='Welcome') }}</h1>"
})
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/templates/home")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { content: "<h1>{{ t.home_hero_title | default(value:'Welcome') }}</h1>" }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/templates/{name}
Delete a custom template override, reverting to the built-in default. Returns 404 if no custom override exists.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/templates/home" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/templates/home",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/templates/home", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/templates/home")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/templates/{name}/preview
Render a template with sample data and return the HTML preview. Validates Tera syntax and reports render errors.
{
"content": "<h1>Welcome to {{ shop_name }}</h1>"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/templates/home/preview" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"content": "<h1>Welcome to {{ shop_name }}</h1>"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/templates/home/preview",
headers={"Authorization": "Bearer zspat_..."},
json={"content": "<h1>Welcome to {{ shop_name }}</h1>"}
)
html = resp.text
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/templates/home/preview", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
content: "<h1>Welcome to {{ shop_name }}</h1>"
})
});
const html = await resp.text();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/templates/home/preview")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { content: "<h1>Welcome to {{ shop_name }}</h1>" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
html = resp.body
/admin/api/email-templates
List all email template names with their override status for the active theme.
[
{
"name": "order_confirmation",
"description": "Order Confirmation",
"has_custom": false,
"updated_at": null
},
{
"name": "shipping_notification",
"description": "Shipping Notification",
"has_custom": true,
"updated_at": "2025-12-05T09:30:00Z"
}
]
curl "https://yourshop.zeroshop.io/admin/api/email-templates" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/email-templates",
headers={"Authorization": "Bearer zspat_..."}
)
templates = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/email-templates", {
headers: { "Authorization": "Bearer zspat_..." }
});
const templates = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
templates = JSON.parse(resp.body)
/admin/api/email-templates/{name}
Get the content of an email template. Returns the custom override if one exists, otherwise the built-in default.
{
"name": "order_confirmation",
"description": "Order Confirmation",
"content": "<html><body><h1>Thank you for your order, {{ customer_name }}!</h1></body></html>",
"is_custom": false
}
curl "https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation",
headers={"Authorization": "Bearer zspat_..."}
)
template = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation", {
headers: { "Authorization": "Bearer zspat_..." }
});
const template = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
template = JSON.parse(resp.body)
/admin/api/email-templates/{name}/default
Get the built-in default content for an email template, ignoring any custom override.
{
"name": "order_confirmation",
"description": "Order Confirmation",
"content": "<html><body>...</body></html>",
"is_custom": false
}
curl "https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/default" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/default",
headers={"Authorization": "Bearer zspat_..."}
)
template = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/default", {
headers: { "Authorization": "Bearer zspat_..." }
});
const template = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/default")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
template = JSON.parse(resp.body)
/admin/api/email-templates/{name}
Create or update a custom email template override. Validates Tera syntax before saving. Email templates are standalone HTML and do not extend the storefront base layout.
{
"content": "<html><body><h1>Order #{{ order_id }} confirmed</h1><p>Thank you, {{ customer_name }}!</p></body></html>"
}
curl -X PUT "https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"content": "<html><body><h1>Order confirmed</h1></body></html>"}'
import requests
resp = requests.put(
"https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation",
headers={"Authorization": "Bearer zspat_..."},
json={"content": "<html><body><h1>Order confirmed</h1></body></html>"}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation", {
method: "PUT",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
content: "<html><body><h1>Order confirmed</h1></body></html>"
})
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation")
req = Net::HTTP::Put.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { content: "<html><body><h1>Order confirmed</h1></body></html>" }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/email-templates/{name}
Delete a custom email template override, reverting to the built-in default. Returns 404 if no custom override exists.
curl -X DELETE "https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.delete(
"https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation",
headers={"Authorization": "Bearer zspat_..."}
)
assert resp.status_code == 204
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation", {
method: "DELETE",
headers: { "Authorization": "Bearer zspat_..." }
});
require "net/http"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation")
req = Net::HTTP::Delete.new(uri)
req["Authorization"] = "Bearer zspat_..."
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/email-templates/{name}/preview
Render an email template with sample order data and return the HTML preview. Sample context includes shop_name, order_id, customer_name, customer_email, items, subtotal, tax_total, total, and shipping address fields.
{
"content": "<html><body><h1>Order #{{ order_id }}</h1><p>Dear {{ customer_name }},</p></body></html>"
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/preview" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"content": "<html><body><h1>Order #{{ order_id }}</h1></body></html>"}'
import requests
resp = requests.post(
"https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/preview",
headers={"Authorization": "Bearer zspat_..."},
json={"content": "<html><body><h1>Order #{{ order_id }}</h1></body></html>"}
)
html = resp.text
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/preview", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({
content: "<html><body><h1>Order #{{ order_id }}</h1></body></html>"
})
});
const html = await resp.text();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/preview")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { content: "<html><body><h1>Order #{{ order_id }}</h1></body></html>" }.to_json
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
html = resp.body
/admin/api/email-templates/{name}/send-preview
Send a real preview email rendered from the supplied draft (or, when `content` is omitted, the active theme's saved override / built-in default). The body uses the same per-template sample context as the in-browser preview, so what the admin sees on screen matches what lands in the inbox. The subject is the production subject prefixed with `[TEST]`.
{
"recipient_email": "owner@example.com",
"content": "<html><body><h1>Order #{{ order_id }}</h1></body></html>"
}
{
"ok": true
}
curl -X POST "https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/send-preview" \
-H "Authorization: Bearer zspat_..." \
-H "Content-Type: application/json" \
-d '{"recipient_email": "owner@example.com"}'
import requests
requests.post(
"https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/send-preview",
headers={"Authorization": "Bearer zspat_..."},
json={"recipient_email": "owner@example.com"}
)
await fetch("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/send-preview", {
method: "POST",
headers: {
"Authorization": "Bearer zspat_...",
"Content-Type": "application/json"
},
body: JSON.stringify({ recipient_email: "owner@example.com" })
});
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/email-templates/order_confirmation/send-preview")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer zspat_..."
req["Content-Type"] = "application/json"
req.body = { recipient_email: "owner@example.com" }.to_json
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
/admin/api/template-variants
List every distinct template-variant slug of the requested kind across all themes, annotated with which themes define each slug and whether the currently active theme is one of them. Used to populate the Template dropdown on product and category edit forms. Results are sorted by slug ascending; the themes array within each entry is sorted by name ascending.
| Name | Type | Required | Description |
|---|---|---|---|
| kind | string | required | Which variant kind to list. `product` lists product/<slug> templates; `category` lists browse/<slug> templates. Any other value is rejected with 400. |
[
{
"slug": "bundle",
"themes": [
"Default",
"Holiday"
],
"in_active_theme": true
},
{
"slug": "limited-edition",
"themes": [
"Holiday"
],
"in_active_theme": false
}
]
curl "https://yourshop.zeroshop.io/admin/api/template-variants?kind=product" \
-H "Authorization: Bearer zspat_..."
import requests
resp = requests.get(
"https://yourshop.zeroshop.io/admin/api/template-variants",
headers={"Authorization": "Bearer zspat_..."},
params={"kind": "product"}
)
variants = resp.json()
const resp = await fetch("https://yourshop.zeroshop.io/admin/api/template-variants?kind=product", {
headers: { "Authorization": "Bearer zspat_..." }
});
const variants = await resp.json();
require "net/http"
require "json"
uri = URI("https://yourshop.zeroshop.io/admin/api/template-variants")
uri.query = URI.encode_www_form(kind: "product")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer zspat_..."
resp = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
variants = JSON.parse(resp.body)
We value your privacy
We use cookies for essential site functionality and, with your consent, analytics to understand how our platform is used. No personal data is shared with third parties. See our Privacy Policy for details.