How to Upload an Email Template in Shopify
How to Upload an Email Template in Shopify
In this guide, we'll walk through customizing Shopify's order confirmation email by replacing it with a Postcards template. Unlike most ESP integrations where you can drop simple tags into your design, Shopify uses Liquid, a templating language that runs at the code level, so this workflow involves a quick round of editing in a text editor before pasting your code into Shopify.
The result is an order confirmation email that looks exactly the way you designed it in Postcards, but pulls in real order data (order number, customer name, addresses, line items, totals, and so on) automatically when sent.
The same general approach works for other Shopify notifications too (shipping confirmation, abandoned checkout, refund notifications, etc.) only the Liquid variables differ. We'll focus on order confirmation here since it's the most commonly customized.
Prerequisites
- A Postcards account
- A Shopify account with admin access
- A text editor like VS Code, Sublime Text, or Notepad (required for editing the HTML)
Step 1: Build and export your Postcards template
Start by building (or opening) your order confirmation template in Postcards. If you're starting from scratch, the Transactional category in the modules panel has ready-made designs with the common pieces you'll need: order summary, item list, totals breakdown, shipping/billing address blocks, and a call-to-action button to view the order. I will use the pre-made module "Transactional 7"

A few things to keep in mind as you design:
- Wherever you'd normally type a hardcoded value like an order number, customer name, address, or price, just leave a placeholder you can easily find later (for example, "Order #12345" or "John Doe"). We'll replace these with Liquid variables in the next steps.
- For the View order button, give it a placeholder link like
#, we'll swap it for the real Shopify order status URL afterward.

- The item list (product image, name, quantity, price) only needs to be designed once. Shopify's Liquid loop will repeat the row automatically for each product in the order.
When your template is ready, click Export in the top right corner and select Download as ZIP.

Make sure Host images/fonts online is toggled on before downloading, this way, all your images load from absolute HTTPS URLs, so you don't need to upload anything to Shopify manually.
Extract the ZIP somewhere easy to find on your computer, then open the index.html file in your text editor.

This is the file we'll be editing in the next steps. You can also double-click it to open in your browser first to confirm everything looks right before moving on.
Step 2: Open the Shopify order confirmation editor
Now we need to get to the page where Shopify lets you edit the order confirmation email's HTML.
From your Shopify admin dashboard:
Click Settings in the bottom-left corner of the sidebar.

In the settings sidebar, click Notifications.

On the Notifications page, click Customer notifications (the first card under the Sender email section).

Find Order confirmation in the list and click it.
This opens the Order confirmation preview page, where Shopify shows you a rendered version of the current default template with sample data. To get into the actual code, click the Edit code button in the top right corner.

You'll land on the Edit Order confirmation page, which has two main areas:
- Email subject — a single field at the top where the subject line lives (currently something like
Order {{name}} confirmed) - Email body (HTML) — the large code editor below, containing Shopify's default Liquid + HTML template
Before changing anything, it's a good idea to copy the entire default code into a separate file as a backup. Select all the code in the editor (Ctrl+A / Cmd+A), copy it, and paste it into a blank text file you keep safe. Shopify also has a built-in safety net, you can click Revert to and then Default to restore the original Shopify template at any time, but keeping your own backup is the fastest way to roll back if anything goes sideways.

Leave this tab open, we'll come back to it after editing your Postcards HTML.
Step 3: Replace placeholders with Liquid variables
Now we get to the main work, opening your index.html in a text editor and swapping the hardcoded values for Shopify's Liquid variables. When the email is sent, Shopify automatically replaces these variables with the real data from the order.
Here's the complete reference of Liquid tags you'll use for an order confirmation email. You'll only need the ones that match elements in your template. Click any item in the left column to jump straight to the part of the guide where it's covered:
| What you want to display | Liquid tag |
|---|---|
| Email title (page title in browser tab) | {{ email_title }} |
| Order number with # prefix (e.g. #1004) | {{ order_name }} |
| Order date (e.g. Dec 31, 2026) | {{ created_at | date: format: 'abbreviated_date' }} |
| Link to the order status page (for the "View order" button) | {{ order_status_url }} |
| Shipping address (full, formatted) | {{ shipping_address | format_address }} |
| Billing address (full, formatted) | {{ billing_address | format_address }} |
| Subtotal | {{ subtotal_price | money }} |
| Shipping cost | {{ shipping_price | money }} |
| Tax | {{ tax_price | money }} |
| Order total (with currency code) | {{ total_price | money_with_currency }} |
| Duties | - |
| Tips | - |
Open your index.html in the text editor and use Find (Ctrl+F / Cmd+F) to locate each placeholder in your design. Replace the placeholder text with the matching Liquid tag. In my case I'll be using Visual Studio Code, this is what my index.html looks like:

Order Number
Find your placeholder order number like Order #12345. In my case line 556:
Order Number
Find your placeholder order number like Order #12345 . In my case line 556:

And replace it with Order {{ order_name }}

Order Date
Find the placeholder order date

And replace it with {{ created_at | date: format: 'abbreviated_date' }}

View order button
The View order button takes the customer to their order status page, where they can track shipping, view order details, and see updates.
Find the View order button's placeholder link in your HTML, href=" #" like we previously added (or search the button's text)

And replace it with href="{{ order_status_url }}" .

Heads-up: depending on your template, you may see this same link appear twice in the code. That's not a mistake, email clients like Outlook render buttons differently than Gmail and Apple Mail, so designers often include two versions of the same button wrapped in MSO conditional comments (<!--[if mso]> and <!--[if !mso]> ).
Each client only renders the one that applies to it, but you'll need to update both href values so the link works no matter where the email is opened. The easiest way is to use Find & Replace (Ctrl+H / Cmd+H) and swap all instances of href="#" with href="{{ order_status_url }}" at once.

Email Title
Find the <title></title> tag in the <head> of your HTML and place {{ email_title }} between the tags so it becomes <title>{{ email_title }}</title> .

Shipping and billing addresses
For the shipping and billing addresses, the format_address filter outputs the entire address as multi-line text, so you don't need separate placeholders for street, city, state, and zip. Shopify will handle the line breaks and formatting automatically.
Search for your shipping and billing address placeholders in your template and replace each whole address block with a single line. Use whichever applies to the section in your template.
Use whichever applies to the section in your template:
{{ billing_address | format_address }}
{{ shipping_address | format_address }}
This is my placeholder Shipping address

Now we replace with {{ shipping_address | format_address }}

Apply the same replacement to the billing address:

Shopify will handle the line breaks and formatting automatically.
Looping through order items
Your Postcards template probably has one or more sample item rows (product image, name, quantity, price). Shopify needs to repeat that row automatically for every item in the customer's order, so we wrap the row in a {% for %} loop.
If your template has multiple sample product rows, delete the extras, the {% for %} loop will generate the right number of rows automatically based on the actual order. Since this template has two product rows, we'll delete the second one.
To find the second product row, use Find (Ctrl+F / Cmd+F) and search for the product name of the second item (in this template, "Cable-Knit Merino Wool", yours will be different depending on the template you used).

Once you've located it, scroll up to find its outer <tr> opening tag, the one sitting at the same indentation level as the first product row.

To find the matching </tr> , the easiest way in VS Code is to use the small arrow/triangle in the gutter next to line e.g. "1850" to collapse the whole <tr> block, that visually shows you exactly where it ends

The matching </tr> closing tag is the one right before </tbody> . Select everything from that outer <tr> to its matching </tr> (including both tags) and delete it.
Now let's find the section in your HTML that represents the first product row (usually a <tr> block). Wrap it with the loop tags like this:
{% for line in subtotal_line_items %}
<tr> <!-- one product row goes here -->
</tr> {% endfor %}

As you can see, we placed {% for line in subtotal_line_items %} before the opening <tr> of the product row, not the header row above it, since we want the loop to only repeat the product, not the column labels. Now we have to place the {% endfor %} after the closing </tr> of that same product row.

Inside the loop, replace the placeholders for image, name, quantity, and price:
Product image
Wrap the <img> tag of your sample product in an {% if %} so it only renders when an image exists, and use Shopify's special image URL filter:
{% if line.image %}
<img src="{{ line | img_url: 'compact_cropped' }}" width="100" alt="">
{% endif %}
Before:

After:

Product name
Use a fallback pattern in case some stores don't return a product title:
{% if line.product.title %}
{{ line.product.title }}
{% else %}{{ line.title }}
{% endif %}
Before:

After:

Handling the product variant lines
If your template's product row includes extra lines under the product name, in this template, "Pink" and "Medium". Those are hardcoded sample variant details (things like color and size). Because they're plain static text rather than Liquid, they'll show up identically on every product in the order, which looks wrong (every item would say "Pink / Medium").
You have two options:
Option 1 — Delete them. If you don't need to show variant details, find those lines inside the product row and remove them. Each one usually sits in its own small <tr> block within the product row's nested table, so collapse and delete the rows containing "Pink" and "Medium".
Option 2 — Make them dynamic. If you do want to show the product's variant (for example "Red / Large"), replace the sample text with Shopify's variant tag:
{{ line.variant.title }}
Shopify outputs the full variant name here (the option values joined together, like Red / Large ). Since one tag covers the whole variant, you'd typically replace just one of the two lines with {{ line.variant.title }} and delete the other.
For most order confirmation emails, Option 1 (deleting them) keeps things clean and simple, unless showing the variant is important for your store.
In my case I'll first replace The sample text "Pink" with Shopify's variant tag:

And then I'll delete the other line's <tr>...</tr> block ("Medium"), since one tag already covers the whole variant.
One thing to keep in mind: if a product has no real variants, Shopify outputs Default Title here. To hide the line in that case, you can wrap it like this:
{% unless line.variant.title == 'Default Title' %}
{{ line.variant.title }}
{% endunless %}

If all your products use proper variants, the plain {{ line.variant.title }} works fine on its own.
Quantity
Find the quantity column in your product row. In our template, it's the hardcoded number 1 inside a <div> styled with grey text (color: #9b9b9b ) and right-aligned. The exact number will depend on what your sample product showed. Replace that hardcoded number with:
{{ line.quantity }}
This pulls the actual quantity the customer ordered for each item.
Before:

After:

Price per item
Find the price value in the same row, sitting in the rightmost column. In our template it's $199 , but again, yours will depend on your sample. Replace it with:
{{ line.price | money }}
The | money filter is important, it tells Shopify to format the number as currency (e.g. $199.00 ) using your store's currency settings. Without it, Shopify would output the raw number in cents (like 19900 ), which wouldn't make sense to customers.
After:

Update the totals section
Below the items table, your template has a totals section with rows for subtotal, gift code, shipping, and tax, followed by the final total. We'll replace each hardcoded value with its matching Liquid tag, and add a couple of optional rows along the way.
Subtotal
Find the subtotal value in your template (in this template it's $840 ) and replace it with:
{{ subtotal_price | money }}
After:

Gift code / Discounts
Your template has a "Gift code" row with a discount value (here, -$75 ). Since not every order has a discount, we want this row to appear only when one is actually applied.
First, find the tag that contains both the label text ("Gift code:") and the discount value (-$75 ). This is the row that holds the whole line together. In this template it's a <tr align="right" valign="top"> .

The easiest way to confirm you've got the right one is to collapse it in your text editor: when collapsed, it should contain both the label and the value. If it only contains the label, scroll up to the outer <tr align="right" valign="top"> .
Once you've found that row, wrap it by placing {% if discounts_savings and discounts_savings > 0 %} on the line right before the opening <tr> , and {% endif %} on the line right after the closing </tr> :

{% if discounts_savings and discounts_savings > 0 %}
<tr align="right" valign="top">
... the whole gift code row ...
</tr>
{% endif %}
Then, inside that row, find the sample discount value (-$75 ) and replace it with the discount tag.
- {{ discounts_savings | money }}
After:

A small tip: Shopify calls this "Discounts" rather than "Gift code," so you may want to update the row's label text to match.
Shipping cost
Find the shipping value (in this template, $15 ) and replace it with:
{{ shipping_price | money }}
After:

Tax
Find the tax value (in this template, $0 ) and replace it with:
{{ tax_price | money }}
After:

Order Total
Find the total value (in this template, $1075 ) and replace it with:
{{ total_price | money_with_currency }}
After:

We use money_with_currency here instead of just money so the grand total clearly shows the currency code (for example, $1,075.00 USD ), which removes any ambiguity for international customers.
Optional: adding duties and tips rows
Shopify orders can also include duties (for certain international orders) and tips, but your template may not have rows for them by default. If your store uses either, you can add them yourself.
The easiest way is to duplicate an existing totals row. Find the Shipping row you just edited, copy the entire row block, and paste it directly below itself. Then update the copy's label and value. Since duties and tips don't apply to every order, wrap each new row in an {% if %} block so it only shows when relevant.
For duties:
{% if current_total_duties and current_total_duties > 0 %}
Duties: {{ current_total_duties | money }}
{% endif %}
Your duplicated row for duties should look like this:

Inside that row, replace the word Shipping for Duties and then replace the shipping value for
{{ current_total_duties | money }}
After:

For tips, follow the exact same steps as duties; paste another copy of the row, change the label to "Tips," and wrap it in its own {% if %} block with these values:
{% if total_tip and total_tip > 0 %}
{{ total_tip | money }}
{% endif %}

If your store doesn't use duties or tips, you can simply skip this part.
Once you've made all your replacements, save the file. You're ready to paste it into Shopify.
Paste your code into Shopify and preview
With all your replacements done and the file saved, head back to the Shopify Edit code page where you left off.
In the code editor, select all the existing code (Ctrl+A / Cmd+A) and delete it, then paste in your finished Postcards HTML. Click Preview in the top right to see how it looks with Shopify's sample order data. You should see your Postcards design with the real order details filled in: order number, date, addresses, the product list, and the totals.
Take a moment to check everything renders the way you expect. If something looks off, jump back to your HTML file, fix it, and paste the updated code in again, Preview is your best friend for catching issues before the email ever reaches a customer.
You can also click Send test email to send yourself a copy and see exactly how it looks in a real inbox.

Here's what my test email looks like once everything is in place:

Once you're happy with how it looks, click Save to make it live. From now on, every order confirmation Shopify sends will use your custom Postcards design.
What we covered
- How to create and export a Postcards template for use as a Shopify order confirmation email.
- How to open and edit Shopify's order confirmation notification code.
- How to replace hardcoded values with Shopify Liquid variables for the order number, date, and addresses.
- How to use a
{% for %}loop to display every item in an order automatically. - How to update the totals section, including subtotal, shipping, tax, and total.
- How to show discounts, duties, and tips conditionally so they only appear when they apply.
- How to preview your template and send a test email before making it live.