Documentation
How Temply works
What the editor gives you, how a template goes from empty to sent, what a brand carries, who on your team can do what, and how your app asks for the finished email.
Introduction
Temply is a template builder for transactional email. You compose an email out of blocks — a logo, a heading, some copy, a button — and Temply writes the HTML underneath. That HTML is built from tables and inline styles rather than divs and a stylesheet, because that is what it takes to hold its shape in Outlook and the other clients that still parse mail the way a browser did in 2005.
It is built for people who send email from their own product: a developer wiring up a welcome mail or a receipt, and the designer or marketer who wants to change the wording without opening a code editor. You do not write HTML, and you do not hand your sending over to anyone — Temply gives you the markup, your own system sends it.
- 1
Email details — subject, sender, preview text
- 2
Brand — the look this template uses
- 3
Content — the email itself, block by block
- 4
Edit / Preview / HTML — the same content, three ways
There are three things you work with.
Templates are the emails themselves. A template holds its content — the blocks in order — along with a name, a subject line, and its own copy of a look. You edit one in the block editor and save it; it stays in your dashboard until you change it again.
Brands are saved looks: page and card colours, button and link colours, corner radius and spacing. Applying a brand copies those settings onto the template, so several templates can share one visual identity without you setting the colours again each time.
The API is how the finished email reaches your app. You request a template by id with an API key, send the data you want dropped into it, and get back rendered HTML ready to hand to your mail provider. The API serves what you last published — edits stay in your draft until you press Publish.
Creating a template
Open Templates in the dashboard and press New template. There is no dialog to fill in — a starter template called Untitled Template is created and the editor opens on it, already holding a logo, a heading, a few paragraphs and a button. Edit it into your own email, or delete the blocks you do not want.
- 1
New template
Templates → New template. A starter template opens in the editor.
- 2
Name and brand
The Subject field names it. The Brand panel sets the look.
- 3
Compose
Type, press
/for a block, drag blocks into order. - 4
Preview and publish
Check it in Preview, then publish. Every publish keeps a version.
Name it in the Email details section. The Subject field is both the subject line the recipient reads and the name the template goes by in your dashboard, so give it something you will recognise in a list. The rest of that section — From name, To, Reply To, and Preview Text — is the envelope around the email.
Pick a look in the Brand section above the content. A new template starts on whichever brand you set as your default; change it here, or adjust the knobs to take the template its own way.
Compose the email in Content. Type into the canvas, press / for a block, drag blocks by the handle in the gutter until the order is right.
The three buttons at the top of the Content section switch what that pane shows: Edit, Preview, and HTML. Preview renders the email exactly as it will be sent, under a mock inbox row so you can see the subject and preview text the way a client lists them. Two controls sit with it — the Preview data panel, which fills your variables and lets you switch conditions on and off, and Forced dark, which redraws the preview the way a client that inverts every email would show it. Neither is saved; both exist so you find the problem before a recipient does.
Edits save as you go, into a draft. Press Publish when it looks right. Every publish keeps a version, on every plan — the last 10, or the last 50 once the workspace has a template pack (100 on Enterprise) — so a change you regret is recoverable from History.
From there the email leaves Temply one of two ways. The HTML view shows the finished source with a Copy HTML button and a Download button beside it — take the file, drop it into whatever sends your mail, and fill the {{placeholders}} yourself. The Text view saves the plain-text alternative the same way.
Need a second pair of eyes first? Share makes a link anyone can open without signing in. It shows the draft as it stands, and you can turn it off whenever you like.
Or let your app fetch it. Every template has an id shown at the top of the editor, in the form tpl_XXXXXXXX. Create a key under Settings → API keys, then post to the render endpoint with the data for this particular send. You get the published version: keep editing and nothing changes for your app until you publish again.
curl -X POST \
-H "Authorization: Bearer tply_live_..." \
-H "Content-Type: application/json" \
-d '{"data":{"firstName":"Ada","isMember":true}}' \
https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_XXXXXXXX/renderThe response carries the rendered html, with your data already in it, ready to hand to your mail provider. Omit the data object and you get the same email with its placeholders intact.
Keys come in two kinds. A live key (tply_live_…) renders what you published and counts toward your plan. Live keys work on every plan, the free trial included. When a trial or plan ends unpaid the workspace turns read-only, and its live keys answer 402 until someone subscribes. A test key (tply_test_…) renders your current draft — published or not — so staging always shows what you are working on. Test keys are free on every plan, keep working when a workspace is read-only, and stop at 1,000 calls a month.
Every call counts, repeats included, so render once and reuse the result where the email is the same — Caching shows how.
Using brands
A brand is a saved look. It holds the page background behind the email, the card background the content sits on, the padding around both, the corner radius, the button background and label colour, and the link colour. It holds no content — a brand never knows what your email says.
Temply ships five presets: Classic, Minimal, Corporate, Warm, and Slate. They are fixed, so you cannot edit or delete one, and they do not count against the brand limit on your plan. Your own brands live beside them on the Brands page. You make one by starting from a preset, changing what you want, and giving it a name.
One brand can be marked as your default, with Set as default — a preset works here as well as one of your own. A new template starts from it: as long as you have not touched the look yet, opening the editor applies your default brand. Delete the brand that is currently the default and it moves to another of your brands, or back to Classic if you have none left.
Applying a brand copies its settings onto the template. There is no live link afterwards. Change a brand later and the templates already using it keep the look they were saved with; you re-apply the brand to pull the change through.
That copy is also what Custom means. The brand selector shows the preset or brand whose settings the template exactly matches. Change any colour, corner, or spacing and it no longer matches anything, so the selector reads Custom — the look now belongs to that template alone. Custom is a state, not something you can pick; it appears in the list only when it is already what you have. Save it as a brand if you want it anywhere else.
Three knobs cover most of the work. Brand color sets the button and link colour together, and picks black or white for the button label depending on which is more readable on it. Corner — Sharp, Soft, or Round — sets the radius on the card and the buttons. Density — Compact or Comfortable — sets the padding inside the card and above and below the email.
Advanced opens the fields underneath, grouped as Page, Card, and Buttons & links. Here you set each colour, padding, and radius on its own — a page background that differs from the card, a button corner that differs from the card corner. Temply flags a colour that would be hard to read against the text it sits behind, including in the forced dark mode some clients apply.
Dark mode
An email has no dark mode of its own. It has the colours you gave it, and a dark inbox is the reader’s setting, not the template’s. A template built on a dark background is a dark theme: still one set of colours, and a client that recolours email will recolour it too, just with a different result. The two are worth keeping apart, because a dark theme is no protection from dark mode.
Temply declares every email as designed for light, color-scheme: light in the document’s head. What that does depends on which of three kinds of client opens it.
| The client | In dark mode | For example |
|---|---|---|
| Reads the declaration | Shows the email exactly as designed, in light or dark mode alike. Without a declaration it would decide for itself, and often invert a text-heavy email. | Apple Mail on iPhone, iPad and Mac |
| Ignores it and recolours | Applies its own dark treatment whatever the email says: some invert everything, some only lighten dark text and darken light backgrounds. | The Gmail app, Outlook on Windows and on phones |
| Leaves email alone | Darkens its own chrome and shows the email as designed. | Gmail in a browser, most webmail |
The declaration only reaches the first kind, and that is the point of it. Those clients open a large share of most lists, and with it they show the email you saw in the editor; without it they would guess from the content, and the guess changes as the content does. Leaving the declaration out would not make the other clients behave the same — each recolours in its own way — it would only give up the one case that can be exact.
For the clients that recolour anyway, Temply helps you see it coming. Forced dark in the Preview view redraws the email the way a client that inverts everything would, with images kept the right way round. The Brand panel warns when a colour pair would lose its contrast under that inversion, using the same maths the preview draws with, and the Checks list carries the same findings so they are on the page while you write. One honest limit: the preview shows the full inversion, the harshest case. A client that only lightens dark text and darkens light backgrounds lands somewhere in between, and a template that reads well under the full inversion reads well there too.
Temply does not produce a second set of colours for dark mode — the prefers-color-scheme styles some clients honour. One set of colours, declared light, is what every recipient gets; what a dark inbox makes of it is the table above. Two things help in every case: a logo saved as a transparent PNG with a little padding, so it survives a dark background, and colours a comfortable distance from pure black and pure white, which inversion sends to their extremes.
Your team
Everything in Temply belongs to a workspace: the templates, the brands, the image library, the API keys and the plan. You name yours when you sign up — usually after your company — and it is yours alone until you invite someone.
Invite people from Settings → Team: an email address each, and they get a link that puts them in the workspace. Anyone you invite can build, edit and publish emails, save brands, upload images, share review links and send test emails — the same product you have, with two exceptions.
Those exceptions are what an admin does: the plan and billing, the API keys, and who is on the team. The person who created the workspace is its admin; an admin can make others admins from the Team page. A member makes the emails; an admin manages the account. There are no other roles.
If you are invited into another workspace — a client’s, say — you switch between them from the top of the sidebar. Each workspace has its own templates, keys and plan; nothing crosses over.
The API
Two endpoints, both under https://temply.tacklabs.co.uk/api/public/v1. One tells you about a template; the other turns it into the email you send. Your app never holds HTML — it asks for the finished email with the data for that one recipient, and hands the answer to your mail provider.
Keys
Every request carries a key as a bearer token: Authorization: Bearer tply_live_…. Create keys under Settings → API keys. A live key renders what you last published and counts toward your plan; live keys work on every plan, the free trial included. A test key (tply_test_…) renders your current draft, published or not, so staging always shows what you are working on; it is free on every plan and stops at 1,000 calls a month. Revoking a key takes effect on the next request.
List templates
GET /templates — every template this key can reach, newest change first, so your app can find them rather than be handed codes by hand. A live key lists what is published; a test key lists the drafts too.
curl -H "Authorization: Bearer tply_live_…" \
https://temply.tacklabs.co.uk/api/public/v1/templates- templates
- One entry per template: id, shortCode, title, previewText, publishedAt, updatedAt — the same fields as the single call.
- mode
- "live" or "test" — which copies the list describes.
Get a template
GET /templates/:id — the id is the tpl_… code shown at the top of the editor. It returns what your app needs to decide whether to re-render: nothing about the content itself.
curl -H "Authorization: Bearer tply_live_…" \
https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_AbCd1234- id
- The internal id. Use shortCode for requests.
- shortCode
- The tpl_… id this template answers to.
- title
- The template’s name, which is also its subject line.
- previewText
- The line inboxes show under the subject.
- publishedAt
- When it was last published; null if never.
- updatedAt
- When the copy this key serves last changed. Cache on this.
- mode
- "live" or "test" — which copy the key is reading.
Render a template
POST /templates/:id/render with a JSON body carrying the data for this send. You get back the email as HTML and as plain text, with your data already in it.
curl -X POST https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_AbCd1234/render \
-H "Authorization: Bearer tply_live_…" \
-H "Content-Type: application/json" \
-d '{"data":{"firstName":"Ada","isMember":true}}'- html
- The full email document, ready to hand to your provider.
- text
- The same email with the markup stripped, for the multipart alternative.
- shortCode
- Echoed back.
- updatedAt
- As on GET — when the served copy last changed.
- mode
- "live" or "test".
The data object
Each key in data matches a variable in the template: {{firstName}} reads data.firstName. Every variable in the template needs a value: a missing one is a 422 that lists what to add, never a silent stand-in — the placeholder set in the editor is for previews only. A pill marked optional renders as nothing when its value is missing. Booleans drive “Show if”: a block gated on isMember is dropped when data.isMember is false and kept when it is true or absent. Omit data entirely and you get the email with every placeholder intact and every block showing — the same thing the editor’s composing view shows.
{
"data": {
"firstName": "Ada",
"isMember": true
}
}A Repeat block reads a list. Its “Repeat over” key names an array in data, and the blocks inside come out once per item: a variable inside the block reads the current item first ({{name}} is items[0].name, then items[1].name…), and falls back to the top level of data when the item has no such field. An empty list, or no key at all, renders the block zero times. A value that is not a list is a 422 naming the key.
{
"data": {
"firstName": "Ada",
"items": [
{
"name": "Notebook",
"price": "£12"
},
{
"name": "Pen",
"price": "£3"
}
]
}
}Send it
Temply stops at the finished email; your provider delivers it. The subject is the template’s title, from the metadata call, and the render’s html and text are the two parts of one multipart message — send both, so inboxes that prefer plain text get the same email. Resend is shown because it is what Temply itself sends through; any provider takes the same three things.
# 1. The subject line
curl -H "Authorization: Bearer tply_live_…" https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_AbCd1234
# → { "title": "Welcome email", ... }
# 2. The email for this recipient
curl -X POST https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_AbCd1234/render \
-H "Authorization: Bearer tply_live_…" \
-H "Content-Type: application/json" \
-d '{"data":{"firstName":"Ada","isMember":true}}'
# → { "html": "<!doctype html>...", "text": "Welcome, Ada...", ... }
# 3. Hand both parts to your provider (Resend shown)
curl -X POST https://api.resend.com/emails \
-H "Authorization: Bearer re_..." \
-H "Content-Type: application/json" \
-d '{"from":"you@example.com","to":"ada@example.com","subject":"Welcome email","html":"...","text":"..."}'Every call counts, so render once per email, not once per recipient, and cache on updatedAt rather than fetching metadata before every send: it moves only when the copy your key serves changes — on publish for a live key, on save for a test key. Caching shows the pattern.
Caching
Every call counts toward the month — a list, a metadata call and a render alike. Asking again for something that has not changed counts the same as the first time; there is no free not-modified answer. What keeps the bill down is calling less, and an email rarely changes between sends.
Render a broadcast once. When many people get the same email, render it once and hand the same html and text to every send: one call, not one per recipient. Render per recipient only where the content differs per recipient.
Cache on updatedAt. Keep each render under the template, the data you sent and the updatedAt that came back with it. updatedAt moves only when the copy your key serves changes — on publish for a live key, on save for a test key — so until it moves, the cached email is the one Temply would give you. Check it with one list call on a schedule or when you deploy, not before every send.
# 1. On a schedule, or when you deploy: one call dates every template
curl -H "Authorization: Bearer tply_live_…" \
https://temply.tacklabs.co.uk/api/public/v1/templates
# → { "templates": [{ "shortCode": "tpl_AbCd1234", "updatedAt": "2026-09-01T09:30:00.000Z", … }] }
# 2. Only where updatedAt moved since you cached it: render once
curl -X POST \
-H "Authorization: Bearer tply_live_…" \
-H "Content-Type: application/json" \
-d '{"data":{"campaign":"autumn"}}' \
https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_AbCd1234/render
# → keep { html, text } with that updatedAt, and send the same html to everyoneNever render on a page view, or on any request a visitor can repeat: a reload or a crawler then spends your calls. Render when you send, from your own server, and reuse what you rendered.
Errors and limits
Every error is JSON with a status, a message you can show, and an errors list. The codes:
- 401
- No key, an unknown key, or a revoked one.
- 404
- No template with that id on this account — or, with a live key, one that has never been published.
- 422
- Data was sent but a variable has no value — the body lists them under missing — or a Repeat’s key holds something other than a list.
- 402
- The workspace’s trial or plan has ended, so its live keys are paused. Nothing is deleted, and the key works again once someone subscribes on the Plan page. Test keys are not affected.
- 429
- Either the key went past its per-minute burst — that answer carries a Retry-After header in seconds — or a workspace on the free trial has used its 10,000 live calls for the month, which lasts until the month turns or someone subscribes. The message says which.
- 500
- The stored template could not be read. Open it in the editor and save.
Calls with a live key count toward a monthly total that resets on the first of each month, UK time. Every call counts — lists, metadata and renders, repeats included — so cache what you can. Test keys have their own 1,000 a month on every plan. On top of the month, one key may make 120 calls a minute (30 for a test key); past that the call is refused without counting, and Retry-After says how long to wait.
Two answers deserve code of their own. A 422 lists the values to add under missing, so the fix is in your data, not a retry. A 429 with Retry-After is the burst: the refused call was not counted, so waiting that long and trying again costs nothing. A 429 without it — the trial’s month is used up — and a 402 do not clear on a retry: someone has to subscribe, so stop and tell a person rather than loop.
curl -i -X POST https://temply.tacklabs.co.uk/api/public/v1/templates/tpl_AbCd1234/render \
-H "Authorization: Bearer tply_live_…" \
-H "Content-Type: application/json" \
-d '{"data":{}}'
# HTTP/2 422
# { "status": 422, "message": "Missing values for: firstName", "missing": ["firstName"] }
# HTTP/2 429
# Retry-After: 12
# { "status": 429, "message": "...", ... }
# HTTP/2 402
# { "status": 402, "message": "...", ... }- Free trial
- 14 days, no card. 10,000 live calls a month, then a 429 until the month turns or someone subscribes. 5 live keys.
- Team
- $5 per user a month. 10,000 live calls a month included, then $1 per 1,000 on the next invoice, with no monthly cap. 5 live keys.
- Enterprise
- Volume agreed with you. Unlimited live keys.
- Read-only
- A trial or plan that ended unpaid. Live keys answer 402; test keys keep their 1,000 calls a month.
The editor
The canvas in the middle is the email at its real width, on the white background a mail client will paint. You type into it directly. Every paragraph, image, and button is a block, and each one has a drag handle in the gutter for moving it up or down the email.
Press / where the next block should go and the slash menu opens. It lists every block; keep typing to filter it, and press Enter to insert the one you want. Select a block and a bubble menu appears with the settings that belong to that block — alignment and colour for text, the URL and label for a button, the source and width for an image. Nothing is buried in a side panel that applies to everything.
Text
Text
A plain paragraph. The default — start typing and you are already in one.
Heading 1
The largest heading. Use it once, for the line that says what the email is about.
Heading 2
A medium heading. Breaks a longer email into sections.
Heading 3
A small heading. For a label above a short run of text, when Heading 2 is too loud.
Bullet List
An unordered list. For items where the order does not matter — features, links, notes.
Numbered List
An ordered list. For steps someone has to follow in sequence.
Blockquote
Indented text with a rule down its left edge. To set a quote or a pulled-out remark apart from the body copy.
Media
Image
A full-width image with its own alignment and link. For a hero image or a screenshot that should span the card.
Logo
An image sized and aligned as a logo rather than as content. At the top of the email, where your mark belongs.
Inline Image
A small image that sits in the flow of a line of text. For an icon or a badge next to words, not on a line of its own.
Link Card
A bordered card with a title, description, image, and link. To point at one thing — an article, a doc, a release — with more weight than a link.
Layout
Columns
Splits the width into columns, each holding its own blocks. For side-by-side content. Many clients collapse them on narrow screens, so keep each column able to stand alone.
Section
A container with its own background, padding, and border around the blocks inside it. To band off part of the email — a highlighted note, a coloured panel.
Spacer
Vertical empty space of a set height. To open up a gap where the default block spacing is too tight.
Divider
A horizontal rule. To mark the seam between two parts of the email — usually before the footer.
Advanced
Button
A call-to-action button built from a table cell, so it renders as a solid block rather than a styled link. For the one action you want the reader to take.
Repeat
Loops the blocks inside it over an array from your data, once per item. For order lines, digest items, or anything whose length you do not know when you build the template.
Custom HTML
Raw HTML dropped into the email as written. Only when no block does what you need. Temply does not fix this markup for you, so it is on you to keep it email-safe.
Components
Headers
Three designed openings: a logo with text stacked or side by side, or a logo over a cover image. To start an email the way most do, then change the words and the picture.
Footers
Three designed closings, in the smaller footer style: a copyright line, a feedback call to action, a company signature. For the address, the unsubscribe line and the legal small print — pick the nearest and edit it down.
The slash menu also carries a Components group with pre-built headers and footers. Each one inserts a small arrangement of the blocks above, which you then edit like anything else.
Shortcuts
The editor answers to more than its menus. Everything below works while the cursor is in the canvas; the same list is a click away in the editor itself, under the question mark beside the view switch.
Insert
- /
- Open the block menu — every block, filtered as you type
- @
- Insert a variable, in text, a button label or a URL
- ---
- Turn the line into a divider
- ⌘⌥C
- Insert a Custom HTML block
Write
- #
- Heading 1 — ## and ### for the smaller ones
- -
- Start a bullet list
- 1.
- Start a numbered list
- >
- Start a blockquote
- **text**
- Bold — *text* italic, `text` code, ~~text~~ struck through
- ⇧Enter
- A new line inside the block, without a new block’s spacing
Blocks
- ⌘⇧↑
- Move the block up
- ⌘⇧↓
- Move the block down
- ⌘⇧D
- Duplicate the block
- ⌘⇧Space
- Select the whole block
- ⌘⇧F
- Move to the menu for the block or the selection; Escape comes back
- ⌘⇧⌫
- Delete the block
Text
- ⌘B
- Bold
- ⌘I
- Italic
- ⌘U
- Underline
- ⌘Z
- Undo — hold Shift as well to redo
Variables
Type @ where the text should change per recipient. A list of the variables already in the template appears; pick one, or type a new name to create it. The variable sits in the copy as a pill you can click, which is also where you set a Placeholder — the words previews, thumbnails and test sends show in its place. A real render never uses it: your data has to carry every value. Button labels and link URLs can be variables too.
In the rendered HTML a variable is a {{name}} placeholder. When you send no data with your request they pass through exactly like that, so you can take the markup and fill it with your own templating instead of Temply's. Send data and each one is replaced by the matching key.
The Preview data panel in the Preview view is where you check the filled version. It lists every variable the template uses; type a value and the preview redraws with real text, so you can see whether a long name breaks a line. Leave one empty and it stays a placeholder. Nothing you type here is saved with the template.
Show if
Select a block, open the eye button in its bubble menu, and put a key under Show if. The block then renders only when that key is true in the data you send. Leave it empty and the block always shows.
The key is a free-text name, not a value chosen from a fixed list. It means whatever the template and the backend doing the sending agree it means, so pick something your own code can answer — isMember, hasUnpaidInvoice. One template then covers several cases instead of splitting into near-identical copies. Keys already used elsewhere in the template are offered as suggestions, and hovering one outlines every block that uses it.
One rule worth knowing: a request that sends no data at all shows every conditional block, because there is nothing to test against. Once you do send data, a key that is missing from it counts as false and the block is dropped. So send the key, even when it is false.
With a block gated on isMember, this request keeps it, and the same request with false — or without the key — drops it:
{
"data": {
"firstName": "Ada",
"isMember": true
}
}Repeat
A Repeat block turns a list in your data into a run of blocks. Insert one from the slash menu, set Repeat over to the key that holds the list, and build one item inside it: a line, a card, a row of columns. On render the block comes out once per item, and a variable pill inside it reads the current item first — so {{name}} is each item’s own name — and the top level of your data when the item has no such field. Anything typed in plainly repeats as written, so a Repeat is only as useful as the pills in it.
In the editor you edit one item, and the rows the list would add are drawn faded beneath it — as many as the sample data says, two unless you change it in the Data panel — so the rhythm of the repetition is on the canvas while you write. The copies follow every keystroke and take no typing of their own: click one and you are back in the row. The marker in the margin carries the count. A Show if on a block inside the repeat is answered by the item too, so one item can hide a line the next one shows.
With Repeat over set to items and a line inside reading {{name}} — {{price}}, this request renders two lines:
{
"data": {
"firstName": "Ada",
"items": [
{
"name": "Notebook",
"price": "£12"
},
{
"name": "Pen",
"price": "£3"
}
]
}
}An empty list, or no items key at all, renders the block zero times. A value that is not a list is refused with a 422 that names the key.