{"title":"Enable printable invoices and receipts","slug":"enable-printable-invoices-and-receipts","url":"https://support.storeconnect.com/articles/enable-printable-invoices-and-receipts","url_markdown":"https://support.storeconnect.com/articles/enable-printable-invoices-and-receipts.md","subtitle":null,"summary":"Add printable invoice and receipt links to the customer account page using Liquid snippets, store variables, and the browser print dialog to generate clean PDFs from order data.","type":"Help_Documentation","video_url":"","keywords":"printable invoice, printable receipt, account page, print invoice, print receipt, PDF invoice, PDF receipt, order invoice, order receipt, liquid snippets, store variables, theme customization, account orders","last_modified":"2026-08-21T07:12:35+0000","body_markdown":"StoreConnect does not generate invoices or receipts as built-in documents, but you can add printable invoice and receipt links to the customer account page using Liquid theme snippets. When a customer clicks the link, a popup opens containing only the printable content (using your theme's existing CSS), and the browser's print dialog opens automatically so they can save to PDF or print directly.\n\n:::note\nThis applies to web orders only, and requires access to your theme's snippet and template files via Theme Builder or a local theme repo. POS receipts and invoices use a separate print template system. See [POS print templates](pos-print-templates).\n:::\n\n## How it works\n\nThe solution uses a URL parameter (`?print=invoice` or `?print=receipt`) on the order detail page to trigger a popup:\n\n1. A snippet (`account/print`) reads the `print` param from the current request. If it matches `invoice` or `receipt`, it finds the matching order and opens a popup window.\n2. The popup copies your theme's active stylesheets so the printable content inherits your store's fonts, colors, and layout.\n3. A second snippet (`orders/printable`) renders the order content (company header, addresses, order items, payment summary) inside the popup.\n4. The popup title is set to `Invoice 00000134` or `Receipt 00000134` so the suggested PDF filename is correct when customers save.\n\n## Before you start\n\n- You have theme access (Theme Builder or a local theme repo) and can add or edit snippets and page templates.\n- You understand the StoreConnect [Liquid object model](order-liquid-object-reference).\n- You have added the following locale keys to your [theme locales](theme-locales):\n\n| Key | Value |\n|-----|-------|\n| `accounts.orders.invoice.heading` | `Invoice %{order_number}` |\n| `accounts.orders.receipt.heading` | `Receipt %{order_number}` |\n\n## Step 1: Create the print controller snippet\n\nCreate `snippets/account/print`. This snippet runs on every account page load but only acts when `?print=invoice` or `?print=receipt` is present and the `section` is `orders`.\n\n\n```liquid\n\n{%- comment -%}\n  Snippet: account/print\n  Inputs: section, identifier, print ('invoice'|'receipt')\n  Opens a clean popup, copies page styles, injects printable HTML,\n  sets the title to '{Type} {OrderRef}' and triggers window.print().\n{%- endcomment -%}\n\n{%- assign _section = section | downcase -%}\n{%- assign _print = print | downcase -%}\n{%- assign _idref = identifier -%}\n\n{%- if _section == 'orders' and _idref != blank and (_print == 'invoice' or _print == 'receipt') -%}\n  {%- assign order = nil -%}\n  {%- for o in current_customer.orders -%}\n    {%- if o.reference == _idref -%}\n      {%- assign order = o -%}\n      {%- break -%}\n    {%- endif -%}\n  {%- endfor -%}\n\n  {%- if order -%}\n    {%- capture __printable -%}\n      {% render \"orders/printable\", order: order, type: _print %}\n    {%- endcapture -%}\n    {%- assign __doc_title = _print | capitalize | append: ' ' | append: order.reference -%}\n\n    \u003cscript\u003e\n      (function () {\n        var w = window.open('', '_blank');\n        if (!w) return;\n\n        var title = {{ __doc_title | json }};\n\n        var headHTML = ''\n          + '\u003cmeta charset=\"utf-8\"\u003e'\n          + '\u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\"\u003e'\n          + '\u003cmeta name=\"robots\" content=\"noindex,nofollow,noarchive\"\u003e'\n          + '\u003ctitle\u003e' + title + '\u003c/title\u003e';\n\n        w.document.open();\n        w.document.write('\u003c!doctype html\u003e\u003chtml\u003e\u003chead\u003e' + headHTML + '\u003c/head\u003e\u003cbody\u003e\u003c/body\u003e\u003c/html\u003e');\n        w.document.close();\n\n        try { w.document.title = title; } catch (e) {}\n\n        // Copy active stylesheets so the popup inherits the store's theme\n        var head = w.document.head;\n        document.querySelectorAll('link[rel=\"stylesheet\"], style').forEach(function (node) {\n          try { head.appendChild(node.cloneNode(true)); } catch (e) {}\n        });\n\n        var content = {{ __printable | json }};\n        w.document.body.innerHTML = content;\n\n        setTimeout(function () { try { w.print(); } catch (e) {} }, 100);\n      })();\n    \u003c/script\u003e\n  {%- else -%}\n    {{ \"accounts.orders.show.not_found\" | t }}\n  {%- endif -%}\n{%- endif -%}\n```\n\n\nThis uses a popup rather than a full-page redirect so your site header, footer, and chat widgets stay out of the printed output. The popup inherits your theme CSS automatically.\n\n## Step 2: Create the printable layout snippet\n\nCreate `snippets/orders/printable`. This snippet renders the actual invoice or receipt content inside the popup. Keep structural CSS (spacing, borders, grids) here and let your theme handle fonts and colors.\n\nThe snippet reads company details from [store variables](store-variables) you define on your store record. Create the following store variables and mark each one as **Available in Liquid**:\n\n| Variable key | Example value |\n|---|---|\n| `company_name` | ACME Pty Ltd |\n| `company_number` | ABN 12 345 678 901 |\n| `company_address` | 123 Main St, Sydney NSW 2000 (multiline) |\n| `invoice_payment_instructions` | Please pay to BSB 123-456, Account 12345678, quoting  (multiline) |\n\nAlternatively, split the address into individual fields (`company_street`, `company_city`, `company_state`, `company_postal_code`, `company_country`). The snippet handles both formats.\n\n\n```liquid\n\n{%- comment -%}\n  Snippet: orders/printable\n  Required: order\n  Optional: type = 'invoice' | 'receipt'\n  Store variables expected:\n    company_name, company_number, invoice_payment_instructions\n    company_address  OR  company_street, company_city, company_state,\n                         company_postal_code, company_country\n{%- endcomment -%}\n\n{%- default order: nil, type: 'invoice' -%}\n{%- unless order == blank -%}\n  {%- assign heading_key = 'accounts.orders.' | append: type | append: '.heading' -%}\n  {%- assign company_name               = store_variables['company_name'] -%}\n  {%- assign company_number             = store_variables['company_number'] -%}\n  {%- assign payment_instructions_raw   = store_variables['invoice_payment_instructions'] -%}\n  {%- assign company_address            = store_variables['company_address'] -%}\n  {%- assign company_street             = store_variables['company_street'] -%}\n  {%- assign company_city               = store_variables['company_city'] -%}\n  {%- assign company_state              = store_variables['company_state'] -%}\n  {%- assign company_postal_code        = store_variables['company_postal_code'] -%}\n  {%- assign company_country            = store_variables['company_country'] -%}\n\n  \u003cstyle\u003e\n    @page { size: A4; margin: 14mm; }\n\n    .SCP-Root {\n      max-width: 210mm;\n      margin: 0 auto;\n      padding: 16mm;\n      box-sizing: border-box;\n      background: #fff;\n    }\n\n    .SCP-Header {\n      display: flex;\n      justify-content: space-between;\n      align-items: flex-start;\n      gap: 16px;\n      margin-bottom: 24px;\n    }\n\n    .SCP-Brand { display: flex; align-items: center; gap: 12px; }\n    .SCP-Logo  { max-height: 48px; display: block; }\n    .SCP-DocTitle { margin: 0; }\n\n    .SCP-Meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin: 12px 0 8px; }\n    .SCP-Meta dt { font-weight: 600; margin: 0; }\n    .SCP-Meta dd { margin: 0; }\n\n    .SCP-Columns { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }\n    .SCP-Section { margin-top: 16px; }\n    .SCP-Box { border: 1px solid; padding: 12px; border-radius: 8px; }\n    .SCP-Muted { opacity: .8; }\n\n    .SCP-Table { width: 100%; border-collapse: collapse; }\n    .SCP-Table th,\n    .SCP-Table td { padding: 8px; border-top: 1px solid; text-align: left; }\n    .SCP-Table thead th { border-top: 0; }\n\n    @media print {\n      .SCP-Root { padding: 0; margin: 0; }\n    }\n  \u003c/style\u003e\n\n  \u003cdiv class=\"SCP-Root\"\u003e\n\n    \u003cdiv class=\"SCP-Header\"\u003e\n      \u003cdiv class=\"SCP-Brand\"\u003e\n        {%- if current_store.logo and current_store.logo.url -%}\n          \u003cimg class=\"SCP-Logo\" src=\"{{ current_store.logo.url }}\" alt=\"{{ current_store.name }}\"\u003e\n        {%- endif -%}\n        {%- if company_name or company_number -%}\n          \u003cdiv\u003e\n            {%- if company_name -%}\u003cstrong\u003e{{ company_name }}\u003c/strong\u003e{%- endif -%}\n            {%- if company_name and company_number -%}\u0026nbsp;\u0026middot;\u0026nbsp;{%- endif -%}\n            {%- if company_number -%}\u003cspan class=\"SCP-Muted\"\u003e{{ company_number }}\u003c/span\u003e{%- endif -%}\n            {%- if company_address or company_street -%}\n              \u003cdiv class=\"SCP-Muted\"\u003e\n                {%- if company_address -%}\n                  {{ company_address | newline_to_br }}\n                {%- else -%}\n                  {%- if company_street -%}{{ company_street | newline_to_br }}\u003cbr\u003e{%- endif -%}\n                  {%- assign _line = '' -%}\n                  {%- if company_city   -%}{%- assign _line = company_city -%}{%- endif -%}\n                  {%- if company_state  -%}{%- unless _line == '' -%}{%- assign _line = _line | append: ', ' -%}{%- endunless -%}{%- assign _line = _line | append: company_state -%}{%- endif -%}\n                  {%- if company_postal_code -%}{%- unless _line == '' -%}{%- assign _line = _line | append: ' ' -%}{%- endunless -%}{%- assign _line = _line | append: company_postal_code -%}{%- endif -%}\n                  {%- unless _line == '' -%}{{ _line }}\u003cbr\u003e{%- endunless -%}\n                  {%- if company_country -%}{{ company_country }}{%- endif -%}\n                {%- endif -%}\n              \u003c/div\u003e\n            {%- endif -%}\n          \u003c/div\u003e\n        {%- endif -%}\n      \u003c/div\u003e\n\n      \u003ch1 class=\"SCP-DocTitle\"\u003e{{ heading_key | t: order_number: order.reference }}\u003c/h1\u003e\n    \u003c/div\u003e\n\n    \u003cdl class=\"SCP-Meta\"\u003e\n      \u003cdt\u003eReference\u003c/dt\u003e   \u003cdd\u003e{{ order.reference }}\u003c/dd\u003e\n      \u003cdt\u003eDate\u003c/dt\u003e        \u003cdd\u003e{{ order.ordered_at | date: \"%-d %b %Y\" }}\u003c/dd\u003e\n    \u003c/dl\u003e\n\n    {%- comment -%} Payment instructions — only shown when no successful payment yet {%- endcomment -%}\n    {%- assign has_success_payment = false -%}\n    {%- for p in order.payments -%}\n      {%- if p.status == 'success' -%}\n        {%- assign has_success_payment = true -%}\n        {%- break -%}\n      {%- endif -%}\n    {%- endfor -%}\n    {%- if payment_instructions_raw and has_success_payment == false -%}\n      \u003cdiv class=\"SCP-Box SCP-Section\"\u003e\n        \u003cstrong\u003ePayment instructions\u003c/strong\u003e\n        \u003cdiv\u003e{{ payment_instructions_raw | newline_to_br }}\u003c/div\u003e\n      \u003c/div\u003e\n    {%- endif -%}\n\n    \u003cdiv class=\"SCP-Columns SCP-Section\"\u003e\n      \u003cdiv class=\"SCP-Box\"\u003e\n        \u003cstrong\u003eShip to\u003c/strong\u003e\n        {%- assign ship_contact = order.ship_to_contact -%}\n        {%- if ship_contact.name -%}\u003cdiv\u003e{{ ship_contact.name }}\u003c/div\u003e{%- endif -%}\n        {%- assign ship_email = ship_contact.email | default: order.checkout_email -%}\n        {%- if ship_email -%}\u003cdiv\u003e{{ ship_email }}\u003c/div\u003e{%- endif -%}\n        {%- if order.shipping_address -%}\n          {%- render \"shared/address_multi_line\", address: order.shipping_address -%}\n        {%- endif -%}\n      \u003c/div\u003e\n\n      \u003cdiv class=\"SCP-Box\"\u003e\n        \u003cstrong\u003eBill to\u003c/strong\u003e\n        {%- assign bill_contact = order.contact -%}\n        {%- if bill_contact.name -%}\u003cdiv\u003e{{ bill_contact.name }}\u003c/div\u003e{%- endif -%}\n        {%- assign bill_email = bill_contact.email | default: order.checkout_email -%}\n        {%- if bill_email -%}\u003cdiv\u003e{{ bill_email }}\u003c/div\u003e{%- endif -%}\n        {%- if order.billing_address -%}\n          {%- render \"shared/address_multi_line\", address: order.billing_address -%}\n        {%- endif -%}\n      \u003c/div\u003e\n    \u003c/div\u003e\n\n    \u003cdiv class=\"SCP-Section\"\u003e\n      {%- render \"orders/order_summary\", order: order, source: order -%}\n    \u003c/div\u003e\n\n  \u003c/div\u003e\n{%- endunless -%}\n```\n\n\n## Step 3: Update the account page template\n\nOpen `templates/pages/account` and add the print snippet render at the top of the file, passing the section, identifier, and print params. Your existing section routing continues to work as before.\n\n\n```liquid\n\n{%- layout \"account\" %}\n\n{%- assign print      = current_request.params.print | downcase -%}\n{%- assign section    = current_request.params.section -%}\n{%- assign identifier = current_request.params.identifier -%}\n\n{% render \"account/print\", section: section, identifier: identifier, print: print %}\n\n{%- case section -%}\n  {%- when \"orders\" -%}\n    {% render \"account/orders\", identifier: identifier %}\n  {%- when \"fulfillments\" -%}\n    {% render \"account/fulfillments\", identifier: identifier %}\n  {%- when \"subscriptions\" -%}\n    {% render \"account/subscriptions\", identifier: identifier %}\n  {%- when \"account_credits\" -%}\n    {%- if current_customer.can_use_account_credit? -%}\n      {% render \"account/account_credits\", identifier: identifier %}\n    {%- else -%}\n      {% render \"account/not_authorised\" %}\n    {%- endif -%}\n  {%- when \"account_points\" -%}\n    {%- if current_customer.can_use_account_points? -%}\n      {% render \"account/account_points\", identifier: identifier %}\n    {%- else -%}\n      {% render \"account/not_authorised\" %}\n    {%- endif -%}\n  {%- when \"product_approvals\" -%}\n    {% render \"account/product_approvals\", identifier: identifier %}\n  {%- when \"credentials\" -%}\n    {% render \"account/credentials\" %}\n  {%- when \"contact\" -%}\n    {% render \"account/contact\" %}\n  {%- when \"shipping\" -%}\n    {% render \"account/shipping\" %}\n  {%- when \"billing\" -%}\n    {% render \"account/billing\" %}\n  {%- else -%}\n    {% render \"account/profile\" %}\n{%- endcase -%}\n```\n\n\n## Step 4: Add invoice and receipt links to the order page\n\nTo give customers a way to trigger printing, add links to your `snippets/orders/show` template (or wherever you display individual order details). Use `order.path` as the base URL:\n\n\n```liquid\n\n\u003ca href=\"{{ order.path }}\u0026print=invoice\"\u003e\n  {{ 'accounts.orders.invoice.heading' | t: order_number: order.reference }}\n\u003c/a\u003e\n\n\u003ca href=\"{{ order.path }}\u0026print=receipt\"\u003e\n  {{ 'accounts.orders.receipt.heading' | t: order_number: order.reference }}\n\u003c/a\u003e\n```\n\n\n## Set up store variables\n\nIn Salesforce, go to your store record and create the following [store variables](store-variables). Set **Available in Liquid** to true for each one.\n\n| Key | Description |\n|-----|-------------|\n| `company_name` | Your business name, shown in the invoice header |\n| `company_number` | Business registration number (e.g., ABN, GST number) |\n| `company_address` | Full address as a single multiline field |\n| `invoice_payment_instructions` | Bank transfer details or other payment instructions, shown only on unpaid invoices |\n\nIf you prefer separate address fields, use `company_street`, `company_city`, `company_state`, `company_postal_code`, and `company_country` instead of `company_address`.\n\n## Testing\n\n1. Log in as a customer who has at least one order.\n2. Navigate to **My Account → Orders** and open an order.\n3. Append `?\u0026print=invoice` or `?\u0026print=receipt` to the URL.\n4. A popup should open and auto-trigger the browser print dialog.\n5. Save as PDF — the browser suggests a filename based on the page title (e.g., `Invoice 00000134.pdf`).\n\n:::tip\nIf the popup is blocked, your browser's popup blocker is preventing `window.open()`. Allow popups for your store domain and try again.\n:::\n\n## Security\n\nThe print snippet only accesses orders belonging to the currently logged-in customer. It loops through `current_customer.orders` and matches on `reference`, so customers cannot access each other's order data."}