Invoices from Shopify orders
Each time an order is paid in your Shopify store, Typefold can render its invoice from a template of your own, ready to email to the customer or store. The template reads the order as Shopify sends it, so there is no data to convert.
Before you start
Section titled “Before you start”- A Shopify store where you can add apps, or a development store to try this on.
- An API key with
render:create,render:readandtemplate:read.
1. Build the invoice template
Section titled “1. Build the invoice template”Make a template called Shopify invoice and use a Shopify order as its sample data. This order
has the fields an invoice usually needs:
{ "id": 6012345678901, "name": "#1001", "order_number": 1001, "created_at": "2026-10-25T14:03:11+02:00", "currency": "EUR", "billing_address": { "name": "Marta Tamm", "address1": "Tartu mnt 2", "zip": "10115", "city": "Tallinn", "country": "Estonia" }, "line_items": [ { "title": "Linen shirt", "quantity": 2, "price": "49.00" }, { "title": "Canvas tote", "quantity": 1, "price": "19.00" } ], "subtotal_price": "117.00", "total_tax": "22.23", "total_price": "139.23"}-
In the visual editor, paste it under Lay out from JSON data in the Data tab, and every field gets a block to style; see sample data.
-
In the code editor, save it as the template’s sample data, as in Build a template, and print its fields:
<h1>Invoice {{name}}</h1><p>{{format created_at type="date" dateStyle="long"}}</p><p>{{billing_address.name}}<br />{{billing_address.address1}}<br />{{billing_address.zip}} {{billing_address.city}}<br />{{billing_address.country}}</p><table>{{#each line_items}}<tr><td>{{title}}</td><td>{{quantity}}</td><td>{{price}} {{../currency}}</td></tr>{{/each}}</table><p>Tax {{total_tax}} {{currency}}<br />Total {{total_price}} {{currency}}</p>
Shopify sends amounts as text, such as "49.00", so print them as they are; the format
helper formats numbers only. Shopify’s webhooks reference
lists every other field of the orders/paid topic, such as shipping_address, discount_codes
and each item’s sku.
Publish the template when it looks right.
2. Render on every paid order
Section titled “2. Render on every paid order”Pick the tool you already use.
- Install the Typefold nodes and connect them, as in n8n.
- Download the Shopify invoice workflow and import it in n8n: open a new workflow, click ⋯ at the top right and choose Import from File.
- Open Shopify Trigger and connect your store; see n8n’s Shopify credentials. It listens for Order Paid.
- Open Typefold and pick your Typefold credential. It renders
Shopify invoicewith the whole order as Data. - Open Send Email, connect your mail server and change From Email. It sends the PDF to the order’s email address. To store the PDF instead, replace it with a Google Drive, Dropbox or S3 node.
- Activate the workflow.
Zapier
Section titled “Zapier”- Connect Typefold, as in Zapier.
- Start a Zap with Shopify and the New Paid Order trigger, and test it to load an order.
- Add Typefold → Render Report and pick
Shopify invoice. Its fields are named after the sample order, such asbilling_address › name; map the order’s values into them. For the items, mapline_items › title,line_items › quantityandline_items › priceto the order’s line item values. - Add an email step, such as Gmail → Send Email, with the Typefold step’s File as the attachment.
Your own code
Section titled “Your own code”- In your Shopify app, subscribe to the
orders/paidtopic with your endpoint’s URL; see Shopify’s webhooks guide. - In the endpoint, check the
X-Shopify-Hmac-SHA256header against the body with your app’s client secret, then answer200within 5 seconds, Shopify’s time limit. Render the invoice after answering, for example from a queue. - Render the template with the order as its data. The order’s
idmakes a good idempotency key, so when Shopify sends an order twice, you get the first invoice back instead of a second one.
curl can’t check Shopify’s signature, but it can render a saved order to try the template:
curl "$PDF_URL/v1/reports/render" \ -H "x-api-key: $API_KEY" \ -H "content-type: application/json" \ --fail-with-body -o invoice.pdf \ -d "{\"template\": {\"name\": \"Shopify invoice\"}, \"data\": $(cat order.json)}"using System.Security.Cryptography;using System.Text;using System.Text.Json;using Typefold.Client;
var client = new TypefoldClient( new Uri(Environment.GetEnvironmentVariable("PDF_URL")!), Environment.GetEnvironmentVariable("API_KEY")!);
// rawBody is the request body exactly as received, hmac the X-Shopify-Hmac-SHA256 header.static bool IsFromShopify(byte[] rawBody, string hmac){ var secret = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SHOPIFY_CLIENT_SECRET")!); var expected = HMACSHA256.HashData(secret, rawBody); var given = new byte[expected.Length];
return Convert.TryFromBase64String(hmac, given, out var written) && written == expected.Length && CryptographicOperations.FixedTimeEquals(given, expected);}
async Task RenderInvoiceAsync(byte[] rawBody){ var order = JsonSerializer.Deserialize<JsonElement>(rawBody);
byte[] pdf = await client.RenderReportAsync( new RenderReportRequest { Template = TemplateRef.ByName("Shopify invoice"), Data = order }, idempotencyKey: $"shopify-order-{order.GetProperty("id")}");
await File.WriteAllBytesAsync($"invoice-{order.GetProperty("order_number")}.pdf", pdf);}import { createHmac, timingSafeEqual } from "node:crypto";import { writeFile } from "node:fs/promises";import { TypefoldClient, TemplateRef } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL!, apiKey: process.env.API_KEY!,});
// rawBody is the request body exactly as received, hmac the X-Shopify-Hmac-SHA256 header.export function isFromShopify(rawBody: Buffer, hmac: string): boolean { const expected = createHmac("sha256", process.env.SHOPIFY_CLIENT_SECRET!) .update(rawBody) .digest(); const given = Buffer.from(hmac, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);}
export async function renderInvoice(rawBody: Buffer): Promise<void> { const order = JSON.parse(rawBody.toString("utf8"));
const pdf = await client.renderReport( { template: TemplateRef.byName("Shopify invoice"), data: order }, { idempotencyKey: `shopify-order-${order.id}` }, );
await writeFile(`invoice-${order.order_number}.pdf`, pdf);}import { createHmac, timingSafeEqual } from "node:crypto";import { writeFile } from "node:fs/promises";import { TypefoldClient, TemplateRef } from "@typefold/client";
const client = new TypefoldClient({ baseUrl: process.env.PDF_URL, apiKey: process.env.API_KEY,});
// rawBody is the request body exactly as received, hmac the X-Shopify-Hmac-SHA256 header.export function isFromShopify(rawBody, hmac) { const expected = createHmac("sha256", process.env.SHOPIFY_CLIENT_SECRET).update(rawBody).digest(); const given = Buffer.from(hmac, "base64");
return given.length === expected.length && timingSafeEqual(given, expected);}
export async function renderInvoice(rawBody) { const order = JSON.parse(rawBody.toString("utf8"));
const pdf = await client.renderReport( { template: TemplateRef.byName("Shopify invoice"), data: order }, { idempotencyKey: `shopify-order-${order.id}` }, );
await writeFile(`invoice-${order.order_number}.pdf`, pdf);}import base64import hashlibimport hmacimport jsonimport osfrom pathlib import Path
from typefold import TypefoldClient, RenderReportRequest, TemplateRef
client = TypefoldClient(os.environ["PDF_URL"], os.environ["API_KEY"])
# raw_body is the request body exactly as received, signature the X-Shopify-Hmac-SHA256 header.def is_from_shopify(raw_body: bytes, signature: str) -> bool: secret = os.environ["SHOPIFY_CLIENT_SECRET"].encode() expected = base64.b64encode(hmac.new(secret, raw_body, hashlib.sha256).digest()).decode()
return hmac.compare_digest(expected, signature)
def render_invoice(raw_body: bytes) -> None: order = json.loads(raw_body)
pdf = client.render_report( RenderReportRequest(template=TemplateRef.by_name("Shopify invoice"), data=order), idempotency_key=f"shopify-order-{order['id']}", )
Path(f"invoice-{order['order_number']}.pdf").write_bytes(pdf)Read the body as raw bytes, before any JSON parsing, or the signature won’t match.
What it costs
Section titled “What it costs”Each invoice is one document of your plan; see pricing. To try
the flow without using documents, turn on Test Render in n8n or Test render in Zapier, or
add "test": true to the request, and every page gets a “TEST” watermark.