Hosted templates
Store the subject and body once, publish a version, and send it by name.
A template is a subject and a body kept on your workspace, with {{ variables }} in them. Your code sends the name and the values, so changing the wording of a receipt is one API call rather than a deploy.
Substitution is {{ name }} and nothing else. There are no conditionals, no loops, and no expressions, because a stored template must never become code we run on your behalf. Anything that needs a decision is a decision your code makes before it sends.
Create a template
curl -X POST https://emails.sh/v1/templates \
-H "Authorization: Bearer $EMAILSSH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome",
"slug": "welcome",
"subject": "Welcome, {{ name }}",
"html": "<p>Hello {{ name }}, your plan is {{ plan }}.</p>",
"publish": true
}'The slug is what your code names. It is stable, so you can rename the template in the dashboard without touching a line of your application. publish: true makes that first version live immediately; leave it out and the template exists with nothing published, and a send that names it is refused with 422 template_not_published.
Versions and publishing
Every edit writes a new version, and a version is never changed after it is written. That is what makes a message sent last March explainable today: the delivery log records the exact version id that produced it, and that row still says what it said.
Saving never publishes. Live sends render the published version and nothing else, so you can edit a password reset at leisure and it reaches nobody until you publish it.
# Save a new draft version. These two paths take the template id
# rather than the slug, which POST /v1/templates returned when you
# created it.
curl -X POST https://emails.sh/v1/templates/3c1a7f92-5b8e-4d61-9a03-7e4c2b8d1f60/versions \
-H "Authorization: Bearer $EMAILSSH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"subject": "Welcome aboard, {{ name }}", "html": "<p>Hello {{ name }}.</p>"}'
# Point live sends at it
curl -X POST https://emails.sh/v1/templates/3c1a7f92-5b8e-4d61-9a03-7e4c2b8d1f60/publish \
-H "Authorization: Bearer $EMAILSSH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version_id": "9f2c1b4e-3d5a-4c7e-8b21-0d6f5a3c1e88"}'Send one
curl -X POST https://emails.sh/v1/emails \
-H "Authorization: Bearer $EMAILSSH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Acme <hello@acme.com>",
"to": ["ada@example.com"],
"template": {
"id": "welcome",
"variables": { "name": "Ada", "plan": "Pro" }
}
}'The template supplies the subject, the HTML, and the text alternative. Anything you also set on the request wins, so passing subject alongside a template overrides just the subject for that one send. template.id takes the slug or the template id, whichever you have to hand.
A missing variable fails the send
If the published version uses a variable and the request supplies no value for it, the send is refused with 422 template_variables_missing, the response names the variables, and nothing is sent. A receipt that reads "Hello ," is worse than a send that failed loudly, so there is no mode in which a blank is substituted.
{
"error": "template_variables_missing",
"missing": ["plan"],
"message": "The published version of template welcome uses plan and the request supplied no value for it. Pass every one in `template.variables`, or give the variable a default on the template. Nothing was sent."
}Give a variable a default on the template when a blank really is acceptable. Previewing is the one place the strict rule relaxes: the dashboard preview and the preview endpoint fill anything you have not supplied with a sample value, so you can see the layout before you have wired the data up. Only previews do that. Sends never do.